文章作者:Tyan
博客:noahsnail.com
Part I. Spring框架综述
Spring框架是一个轻量级的解决方案,对于构建一个企业级应用来说,Spring框架也是一种可能的一站式服务。Spring是模块化的,允许你仅使用你需要的那部分功能,而不必引入其它的部分。你可以在任何web框架上使用IoC容器,也可以只使用Hibernate集成代码或JDBC抽象层。Spring框架支持声明式的业务管理,支持通过RMI或web service来远程访问你的逻辑,并且支持多种持久化数据的方式。Spring提供一个全功能的MVC框架,使你能将AOP透明的集成到软件中。
Spring被设计为非侵入式的,这意味着你自己的逻辑代码通常情况下不依赖于框架本身。在你的集成层(例如数据访问层),将会存在一些数据访问技术的依赖和Spring的库。不管怎样,从你其余的代码中分离这些依赖应该是很容易的。
这篇文档是Spring框架功能的参考手册。如果你有任何关于这篇文档的要求、评论或问题,请向用户邮寄列表中的人发邮件。关于框架本身的问题可以在StackOverflow上提问。
1.开始学习Spring
这本参考手册提供了关于Spring框架的详细信息,它提供了关于所有功能的全面文档,也介绍了Spring中的基本概念(例如依赖注入)的一些背景。
如果你刚开始学习Spring,你可能想创建一个基于Spring Boot的应用,Spring Boot提供了一个快速(和武断的)方式来创建一个用于生产环境的基于Spring的应用。它是基于Spring框架的,支持约定大于配置,被设计为可以快速启动并且尽可能快的运行起来。
你可以用start.spring.io 来生产一个基本的工程或遵循『Getting Started』指南中的一个,例如『Started Building a RESTful Web Service』指南。除了容易理解吸收之外,这些指南主要是基于任务的,它们中的大多数是基于Spring Boot的。它们也包含了Spring的其它工程,当解决一个特定问题时你可能会考虑它们。
文章作者:Tyan
博客:noahsnail.com
2.Spring框架介绍
Spring框架是一个为支持开发Java应用提供全面基础架构的Java平台。Spring处理基础架构,因此你可以集中精力在你有应用上。
Spring使你能创建简单Java对象(POJO)并能非侵入式的将企业服务应用到简单Java对象(POJO)上。
作为一个应用开发者,下面是一些你能从Spring平台受益的例子:
- 在一个数据库业务中执行一个Java方法而不必处理业务APIs
- 使一个本地的Java方法可以远程调用而不必处理远程APIs
- 使一个本地Java方法变为管理操作而不必处理JMX APIs
- 使一个本地Java方法变为消息处理器而不必处理JMS APIs
2.1依赖注入和控制反转
Java应用——一个不精确的术语,既可以表示受限制的嵌入式应用又可以表示N层服务端的企业级应用——通常由许多对象构成,这些对象协作形成完整的应用程序。因此一个应用程序中的对象是相互***依赖***的。
尽管Java平台提供了大量的应用开发功能,但是它缺少把这些基本构建模块组织成一个连贯整体的方法,并把组织基本构建模块的任务留给了架构师和开发者。虽然你可以使用设计模式例如***工厂模式、抽象工厂模式、生成器模式、装饰模式、服务定位模式***来创建构成应用的各种类和对象实例,但这些设计模式很简单:命名的最佳方法、模式的作用描述、应用模式的位置、模式解决的问题等等。模式使最佳实践形式化了,这意味着你必须在你的应用中***自己实现它***。
Spring框架中的***控制反转***(IoC)组件通过提供一种形式化方法解决了这个问题,这个形式化方法将不同的组件创建到一个随时可用的完整的工作应用中。Spring框架将形式化的设计模式编码成了你可以集成到你自己的应用中的最好对象。许多组织和机构用这种方式应用Spring框架来构建鲁棒的、***可维护***的应用。
背景
“问题是什么是控制反转?” 2004年Martin Fowler在他的网站上提出了这个关于控制反转(IoC)问题。Fowler建议重新命名这个原理使它更一目了然并且提出了依赖注入。
2.2 模块
Spring框架包含的功能大约由20个模块组成。这些模块按组可分为核心容器、数据访问/集成,Web,AOP(面向切面编程)、设备、消息和测试,如下图所示。
图2.1 Spring框架概述
接下来的章节列出了每个功能可用的模块、它们的工件名字以及它们包含的主题。工件名字与依赖管理工具中使用的***artifact IDs***有关。
2.2.1 核心容器
核心容器功能包括`spring-core`, `spring-beans`, `spring-context`, `spring-context-support`, and `spring-expression`(Spring表现语言)模块。
`spring-core`和`spring-beans`模块提供了框架的基础结构部分,包含控制反转(IoC)和依赖注入(DI)功能。`BeanFactory`是工厂模式的高级实现。它去掉了程序单例模式的需求并且允许你从实际的程序逻辑中解耦配置和依赖的指定。
上下文(`spring-context`)模块建立在由Core模块和Beans模块提供的坚实基础上:它是在类似于JNDI注册表式的框架风格模式中访问对象的一种方法。上下文模块继承了Beans模块的功能,并添加了对国际化(例如使用资源捆绑)、事件传播、资源加载和上下文透明创建(例如通过Servlet容器)的支持。上下文模块也支持Java EE功能例如EJB,JMX和基本的远程。`ApplicationContext`接口是上下文模块的焦点。`spring-context-support`支持将第三方库集成进Spring应用程序上下文中,特别是缓存(EhCache, JCache)和定时执行(CommonJ, Quartz)。
2.2.2 面向切面编程(AOP)和设备(Instrumentation)
`spring-aop`模块提供了***AOP*** Alliance-compliant(AOP联盟)面向切面编程的实现,例如允许你自定义方法拦截器和切入点来清晰的解耦功能实现上应该分开的代码。使用源码级的元数据功能,你也可以将行为信息合并到你的代码中,在某种程度上这类似于.NET的属性值。
独立的`spring-aspects`模块提供了与AspectJ的集成。
`spring-instrument`模块提供了类设备支持和类加载器的实现,它们可以在某些应用服务器中使用。`spring-instrument-tomcat`模块包含了Tomcat的Spring设备代理。
2.2.3 消息
Spring 4框架中包含了`spring-messaging`模块,它对***Spring集成***项目例如`Message`, `MessageChannel`, `MessageHandler`和其它作为消息应用服务基础的项目进行了重要的抽象。这个模块也包含了一系列将消息映射到方法上的注解,这个注解与基于编程模型Spring MVC注解类似。
2.2.4 数据访问/集成
***数据访问/集成***层包括JDBC,ORM,OXM,JMS和事务模块。
`spring-jdbc`模块提供了JDBC抽象层,不需要再编写单调的JDBC代码,解析数据库提供商指定的错误编码。
`spring-tx`模块为实现指定接口和所有的简单Java对象(POJOs)的类提供编程式(programmatic)和声明式(declarative)的业务管理。
`spring-orm`模块提供流行的对象关系映射APIs的集成层,包括JPA和Hibernate。在使用`spring-orm`模块时,你可以将Spring的其它功能与这些O/R-mapping框架结合起来使用,例如前面提到的简单声明式业务管理的功能。
`spring-oxm`模块提供对Object/XML映射实现例如JAXB,Castor,JiBx和XStream的抽象层。
`spring-jms`模块(Java消息服务)包含产生和处理小心的功能。从Spring 4.1框架开始它提供了与`spring-messaging`的集成。
2.2.5 网络
网络层包含`spring-web`, `spring-webmvc`和`spring-websocket`模块。
`spring-web`模块提供基本的面向网络集成功能,例如multipart文件上传功能,使用Servlet监听器来初始化Ioc容器和面向网络的应用程序上下文。它也包含了HTTP客户端和Spring远程支持中网络相关的部分。
`spring-webmvc`模块(也被称为***Web-Servlet***模块)包含了Spring的model-view-controller(MVC)和REST Web Services的网络应用实现。Spring的MVC框架提供了对域模型代码,web表单,Spring框架其他功能的完全分离。
2.2.6 测试
`spring-test`模块支持单元测试,Spring组件和JUnit或TestNG的集成测试。它提供了Spring的`ApplicationContexts`加载和这些上下文缓存的一致。它也提供了可以单独测试代码的模拟对象。
2.3 使用场景
前面描述的构建模块使Spring在许多场景中都有一个合理选择,从运行在资源受限的嵌入式应用到全面成熟的企业级应用都在使用Spring的业务管理功能和网络框架集成。
图2.2标准成熟的Spring web应用
Spring的声明式业务管理功能使web应用全面的事务化,如果你用过EJB容器管理业务的话你会发现它们基本一样。你所有自定义的业务逻辑都可以用POJOs实现并通过Spring的IoC容器管理。附加业务包括支持邮件发送和验证,这个是独立于web层之外的,你可以自由选择验证规则执行的位置。Spring对ORM的支持与JPA和Hibernate进行了集成;例如,当你使用Hibernate时,你可以继续使用你现有的映射文件和标准的Hibernate `SessionFactory`配置。表单控制器被无缝的将web层和领域模型进行了集成,对于你的领域模型来讲不再需要`ActionForms`或其它的将HTTP参数转换成值的类。
图2.3. 使用第三方web框架的Spring中间层
有时候环境不允许你完全转成一个不同的框架。Spring框架***不***强迫你都采用它内部的东西;它不是一个***要么全有要么全无***的解决方案。现有的采用Struts,Tapestry,JSF或其它UI框架构建的前端可以与基于Spring的中间层进行集成,这可以让你使用Spring的业务功能。你只需要简单的用`ApplicationContext`和`WebApplicationContext`绑定你的业务逻辑然后集成到web层即可。
图2.4. 远程应用场景
当你需要通过web服务访问现有代码时,你可以使用Spring的`Hessian-`, `Rmi-` 或 `HttpInvokerProxyFactoryBean`类。这能让远程访问现有应用变得很容易。
Figure 2.5. EJBs-包装现有的POJOs
Spring框架也为企业JavaBeans提供了访问和抽象层,使你能重用你现有的POJOs,为了可扩展使用可以将它们包装成无状态的session beans,自动防故障的web应用可能需要声明安全。
2.3.1 依赖管理和命名约定
依赖管理和依赖注入是完全不同的两件事。为了能你的应用中使用Spring的优秀特性(像依赖注入),你需要收集所有必要的库(jar文件)并在运行时将它们添加到classpath中,有可能在编译时就需要添加。这些依赖不是要被注入的虚拟组建,而是文件系统中的物理资源(通常情况下)。这些依赖管理的过程包括资源的定位、存储和添加到classpath中。依赖可以是直接的(例如:我的应用在运行时依赖Spring),或间接的(例如:我的应用依赖`commons-dbcp`,而它依赖`commons-pool`)。间接依赖也被称为"传递式"的,这些依赖也是最难识别和管理的。
如果你想使用Spring,你需要有包含你需要的Spirng功能的jar库副本。为了使这个更容易,Spring被打包成了一系列尽可能将依赖分离开的模块,例如你不想写web应用那你就不需要spring-web模块。为了在本指南中谈及Spring的库模块,我们使用了一个简写命名约定`spring-*`或`spring-*.jar`,`*`表示模块的简写名字(例如`spring-core`, `spring-webmvc`, `spring-jms`等等)。实际中你使用的jar文件名字通常是模块名加上版本号(例如`spring-core-5.0.0.BUILD-SNAPSHOT.jar`)。
Spring框架的每次发布都会下面的地方公布artifacts:
- Maven Central,Maven查询的默认仓库,使用时不需要任何特定的配置。Spring依赖的许多共通库也可以从Maven Central获得,Spring社区的很大一部分都在使用Maven进行依赖管理,因此这对他们来说是很方便的。jar包的命名形式是
spring-*-<version>.jar
,Maven GroupId是org.springframework
。 由Spring掌管的公开Maven库。除了最终的GA release(公开可获得的版本)之外,这个仓库也有开发版本的快照和milestone版本。jar包的命名形式和Maven Central一样,这是一个可以使用Spring开发版本有用地方,而其它的库部署在Maven Central。这个库也包含的捆绑分布的zip文件,这个zip文件中所有的Spring jar包被捆绑到一起很容易下载。
你将在下面找到Spring artifacts列表。想要每个模块更全面的描述,请看2.2小节。
表 2.1. Spring Framework Artifacts
Spring依赖和依赖Spring
虽然Spring提供集成并支持大范围内的企业和其它外部工具,但它有意使它的强制性依赖到一个绝对最小化的程度:对于简单的用例你不应该为了使用Spring而定位和下载(即使是自动的)许多jar库。对于基本的依赖注入仅有一个强制性的外部依赖,那个依赖是关于日志的(在下面可以看到日志选择更详细的描述)。
接下来我们概述配置一个依赖于Spring的应用需要的基本步骤,首先Maven的,其次是Gradle的,最后是Ivy的。在所有的案例中,如果有任何不清楚的地方,请参考你的依赖管理系统的文档,或者看一些示例代码——Spring本身构建时使用Gradle来管理依赖,我们例子中大多数是使用Gradle和Maven的。
Maven依赖管理
如果你正在使用Maven来进行依赖管理,那你不必显式的提供日志依赖。例如,为了创建一个应用上下文,使用依赖注入来配置一个应用,你的Maven依赖看上去是这样的:
1 | <dependencies> |
就是它。注意如果你不需要编译Spring APIs,scope可以被声明成rumtime,这是典型的基本依赖注入的情况。
上面的例子是采用Maven中心仓库的。为了使用Spring Maven仓库(例如:使用milestone版本或snapshot版本),你需要在Maven配置中指定仓库的位置,完整的版本:
1 | <repositories> |
对于milestone版本:
1 | <repositories> |
对于snapshot版本:
1 | <repositories> |
Maven “材料清单” 依赖
在使用Maven时,有可能会偶然的将不同版本的Spring JARs混合起来。例如,你可能找到一个第三方库,或另一个Spring项目,通过传递依赖进入了一个更旧的版本。如果你忘了自己显式的声明一个直接依赖,会产生各种意想不到的问题。
为了解决这种问题,Maven支持"材料清单"(BOM)依赖的概念。你可以在你的`dependencyManagement`部分导入`spring-framework-bom`来确保所有的Spring依赖(直接和传递的)都是同一个版本。
1 | <dependencyManagement> |
使用BOM的额外好处是当依赖Spring框架的artifacts时你不再需要指定`<version>`属性:
1 | <dependencies> |
Gradle依赖管理
为了在Gradle构建系统中使用Spring仓库,在`repositories`部分需要包含合适的URL:
1 | repositories { |
当合适的时候你可以修改`repositories`的URL从`/release`到`/milestone`或`/snapshot`。一旦一个仓库被配置了,你可以用通常的Gradle方式声明依赖:
1 | dependencies { |
Ivy依赖管理
如果你更喜欢使用Ivy来管理依赖,这有类似的配置选择。
为了配置Ivy指定Spring仓库,添加下面的解析器到你的`ivysettings.xml`:
1 | <resolvers> |
当合适的时候你可以更改根URL从`repositories`的URL从`/release`到`/milestone`或`/snapshot`。
一旦配置了,你可以通过一般的方式添加依赖。例如(在`ivy.xml`):
1 | <dependency org="org.springframework" |
发行版Zip文件
尽管使用一个支持依赖管理的构建系统是获得Spring框架的推荐方式,但仍然可以下载发行版的Zip文件。
发行版的zips是被发布到Spring Maven仓库(这只是为了我们的方便,为了下载它们你不需要Maven或任何其它的构建系统)。
为了下载发行版zip,打开浏览器输入[http://repo.spring.io/release/org/springframework/spring](http://repo.spring.io/release/org/springframework/spring),然后选择你想要的版本的合适子文件夹。发行版文件以`-dist.zip`结尾,例如spring-framework-{spring-version}-RELEASE-dist.zip。发行版也可以公布milestone版本或snapshots版本。
2.3.2 日志
日志对于Spring来说是一个非常重要的依赖,因为:*a)*它是唯一的强制性外部依赖,*b)*每个人都喜欢从他们使用的工具中看到一些输出,*c)*Spring集成了许多其它的工具,这些工具也选择了日志依赖。应用开发者的一个目标就是对于整个应用来讲,经常要有一个中心地方来进行日志的统一配置,包括所有的外部组件。比它更困难的可能是有太多的日志框架去选择。
Spring中的强制日志依赖是Jakarta Commons Logging API (JCL)。我们编译JCL并使JCL`log`对象对类是可见的,这扩展了Spring框架。所有版本的Spring采用同一个日志库:移植是容易的,因为即使应用扩展了Spring但保留了向后兼容性,这一点对用户来说很重要。我们实现这个的方式是让Spring的模块之一显式的依赖`commons-logging`(JCL的标准实现),然后使其它模块在编译时依赖这个模块。例如如果你在使用Maven,想找出依赖于`commons-logging`的依赖在哪,它在Spring中,更确切的说它是在Spring的中心模块`spring-core`中。
关于`commons-logging`的一件好事是要使你的应用工作你不需要任何其它的东西。它有一个运行时发现算法,这个算法能寻找其它的日志框架在知名的classpath中,并使用一个它认为是合适的(或者你告诉它你想用哪个如果你需要的话)。如果找不到任何别的你可以从JDK中找到一个非常美好漂亮的日志(java.util.logging或缩写为JUL)。在大多数环境中你可以发现你的Spring应用恰当地运行并输出日志到控制台输出框中,那是很重要的。
不使用Commons Logging
不幸的是, 虽然`commons-logging`的运行时发现算法对于终端用户是方便的,但它是有问题的。如果我们将时钟回拨,把Spring作为一个新项目重新开始,将会选择一个不同的日志依赖。第一个选择可能是Simple Logging Facade for Java(SLF4J),应用内部使用Spring的人使用的许多其它工具也用了SLF4J。
这儿有两种方式关掉`commons-logging`:
- 从
spring-core
模块排除依赖(因为它是唯一的显式依赖)commons-logging
的模块 依赖于一个特定的
commons-logging
依赖,用一个空jar替换这个依赖(更多细节可以在SLF4J FAQ中找到)。为了排除`commons-logging`,把下面的内容加入到你的`dependencyManagement`部分:
1 | <dependencies> |
现在这个应用可能是坏了的,因为在classpath中没有JCL API的实现,为了解决这个问题必须提供一个新的实现。在接下来的部分我们将向你展示怎样提供一个JCL替代实现,使用SLF4J就是一个例子。
使用SLF4J
SLF4J是一个更纯净的依赖并且在运行时比`commons-logging`更有效,因为它使用编译时绑定来代替运行时查找集成的其它日志框架。这也意味着你必须更清楚你想要运行时发生什么,然后相应的声明它或配置它。SLF4J提供跟许多常用日志框架的绑定,因此你通常可以选择一个你正在使用的日志框架,然后绑定到配置和管理上。
SLF4J提供跟许多常用日志框架的绑定,包括JCL,它做的恰恰相反,建立其它日志框架和它自己的纽带。因此为了在Spring中使用SLF4J,你需要用SLF4J-JCL连接器取替换`commons-logging`依赖。一旦你在Spring内部使用了日志调用,Spring会将日志调用变为调用SLF4J API,如果你应用中其它的库调用了那个API,你将有一个单独的地方配置和管理日志。
一个常用的选择连接Spring和SLF4J,然后提供SLF4J到Log4J的显式绑定。你需要提供四个依赖(排除现有的`commons-logging`):连接、SLF4J API、到Log4J的绑定、Log4J本身的实现。在Maven中你可能这么做:
1 | <dependencies> |
这可能看起来为了得到一些日志需要很多依赖。还好它是可选的,比起`commons-logging`的关于类加载器的问题,尤其是你在一个像OSGi平台那样严格的容器中的时候,它应该更好操作。据说这儿也有一个性能提升,因为绑定是在编译时而不是在运行时。
在SLF4J用户中,一个更通用的选择是直接绑定到[Logback](http://logback.qos.ch/),这样使用步骤更少且依赖也更少。这去除了外部绑定步骤,因为Logback直接实现了SLF4J,因此你仅需要依赖两个库而不是四个(`jcl-over-slf4j`和`logback`)。如果你这样做的话你可能也需要从其它的外部应用中(不是从Spring)排除slf4j-api依赖,因为你在classpath中仅需要一个版本的API。
使用Log4J
许多人使用Log4j作为配置和管理的日志框架。它有效且完善的,当我们构建和测试Spring时,实际上这就是在运行时我们使用的东西。Spring也提供一些配置和初始化Log4j的工具,因此在某些模块有可选的Log4j的编译时依赖。
为了使Log4j能与默认的JCL依赖(`commons-logging`)一起工作,所有你需要做的是把Log4j放到classpath中,并提供一个配置文件(`log4j.properties`或`log4j.xml`在classpath的根目录)。对于Maven用户依赖声明如下:
1 | <dependencies> |
下面是一个log4j.properties输出日志到控制台的样本:
运行时容器使用本地化JCL
许多人在容器中运行他们的Spring应用,容器本身提供了一个JCL实现。IBM Websphere Application Server (WAS) 是原型。这经常会引起问题,不幸的是没有一劳永逸的解决方案;在大多数环境下简单的执行`commons-logging`是不够的。
为了使这个更清楚:报告的问题本质上一般不是关于JCL的,或关于`commons-logging`的:而是他们去绑定`commons-logging`到其它的框架上(通常是Log4j)。这可能会失败因为`commons-logging`在一些容器的旧版本(1.0)和大多数人使用的现代版本(1.1)中改变了运行时发现方式。Spring不使用JCL API的和任何不常用的部分,因此不会有问题出现,但是一旦Spring或你的应用试图去输出日志,你可能发现到Log4j的绑定是不起作用的。
在这种情况下使用WAS最容易做的事是逆转类加载层(IBM称为"parent last"),为的是应用能控制依赖,而不是容器。虽然这种选择并非总是公开的,但在公共领域对于替代方法有许多其它的建议,你的解决这个问题花的时间可能是不同的,这取决于确定的版本和容器集合的特性。
Part II. 核心技术
这部分参考文档包含了所有完全集成到Spring框架中的那些技术。
在这些中最重要的是Spring框架的控制反转(IoC)容器。对Spring框架IoC容器的彻底处理是紧随其后的Spring面向切面编程(AOP)技术的全面覆盖。Spring框架有它自己的AOP框架,这在概念上很容易理解,在Java企业级开发中成功了解决了AOP需求中80%的关键点。
Spring也提供了AspectJ的全面集成(目前是最丰富的-考虑到功能-并且确定在Java企业中是最成熟的AOP实现)。
- Chapter 3, The IoC container
- Chapter 4, Resources
- Chapter 5, Validation, Data Binding, and Type Conversion
- Chapter 6, Spring Expression Language (SpEL)
- Chapter 7, Aspect Oriented Programming with Spring
- Chapter 8, Spring AOP APIs
3. IoC 容器
3.1 Spring IoC容器和beans的介绍
这一章包含了Spring框架的控制反转(IoC)原理的实现。IoC也被称为依赖注入(DI)。它是一个处理过程,凭借对象之间依赖关系,也就是和它们一起工作的其它对象,只能通过构造函数参数,传递参数给工厂方法,在构造完成或工厂方法返回的对象实例之后再设置对象实例的属性。当创建bean时容器再将这些依赖对象注入进去。这个过程从根本上颠倒了bean本身通过直接构建类或一种机制例如服务定位模式来控制依赖对象的实例化或定位,因此命名为控制反转(IoC)。
org.springframework.beans
和org.springframework.context
包是Spring框架控制反转容器的基础。BeanFactory
接口提供了一种能管理任何类型对象的高级配置机制。ApplicationContext
是BeanFactory
的一个子接口。ApplicationContext
增加了更容易集成Spring AOP功能;消息资源处理(用在国际化中),事件发布;应用层特定上下文例如WebApplicationContext
在web应用中的使用。
总之,BeanFactory
提供了配置框架和基本功能,ApplicationContext
增加了更多企业专用的功能。ApplicationContext
是BeanFactory
的一个全面超集,在这章仅仅是用来描述Spring的IoC容器。关于用BeanFactory
代替ApplicationContext
的更多信息请参考3.16小节”The BeanFactory”。
在Spring中,被Spring IoC容器管理的那些形成你应用主干的对象被称为beans。bean是实例化、组装、以及其它的都被Spring IoC容器管理的对象。另外,bean仅仅是你应用中许多对象中的一个。Beans和它们之间的依赖关系,通过容器使用的配置元数据可以反映出来。
3.2 容器概述
`org.springframework.context.ApplicationContext`接口代表了Spring IoC容器并且负责实例化、配置和组装前面提到的beans。容器通过读取配置元数据得到说明什么对象要实例化、配置和组装。配置元数据可以用XML、Java注解或Java代码表示。它允许你表示构成应用的对象和对象间丰富的依赖关系。
Spring提供了一些可以直接使用的`ApplicationContext`接口实现。在单独的应用中通常是创建一个 `ClassPathXmlApplicationContext`实例或`FileSystemXmlApplicationContext`实例。虽然XML是定义配置元数据的传统格式,但你可以指示容器支持使用Java注解或代码作为元数据的格式并通过提供少量的XML配置声明使容器支持这些额外的元数据格式。
在大多数应用场景中,不会要求用户用显式的代码来实例化一个或多个Spring IoC容器的。例如,在web应用场景中,在应用的`web.xml`文件中写一个简单的八行左右的样板web描述符XML就足够了(看3.13.4小节,『web应用中ApplicationContext的方便实例化』)。如果你正在使用Eclipse支持的Spring Tool Suite开发环境,可以很容易的通过点几下鼠标或键盘来创建样本配置。
下面的图是从一个高层次的视野来看Spring是如何工作的。你的应用类与配置元数据结合起来为的是在`ApplicationContext`创建和初始化之后,你有一个完整配置并可执行的系统或应用。
图3.1. Spring IoC容器
3.2.1 配置元数据
如上图所示,Spring IoC容器使用了一种*配置元数据*的方式;配置元数据表示你作为一个应用开发者应该告诉Spring容器怎样去实例化、配置并组装应用中的对象。
习惯上用简单直观下XML形式来提供配置元数据,这一章大部分使用XML文件来表达Spring IoC容器的核心概念及功能。
基于XML的元数据不是配置元数据的唯一许可形式。Spring IoC容器本身与配置元数据的实际书写形式是完全解构的。目前许多开发者在他们的Spring应用中选用基于Java配置的元数据形式。
关于Spring容器中使用其它元数据形式的信息,请看:
- 基于注解的配置:Spring 2.5引入对了基于注解的配置元数据的支持。
基于Java的配置:从Spring 3.0开始,Spring JavaConfig工程提供的许多功能开始成为Spring框架核心中的一部分。因此你可以通过Java而不是XML文件来定义外部应用程序的beans。为了使用这些新功能,请看
@Configuration
,@Bean
,@Import
和@DependsOn
注解。Spring配置包括至少一个且通常不止一个容器必须管理的bean定义。基于XML的配置元数据中,这些beans作为`<bean>`元素被配置在顶层`<beans/>`元素中。Java配置通常在`@Configuration`类中使用`@Bean`注解的方法。 这些bean定义与组成你应用的实际对象相对应。通常你会定义服务层对象,数据访问层对象(DAOs),描述对象例如Struts的`Action`实例,底层对象例如Hibernate的`SessionFactories`,JMS的`Queues`等等。容器中细粒度的领域对象通常是不配置的,因为一般是由DAOs和业务逻辑负责创建和加载领域对象。然而你可以使用Spring集成的AspectJ去配置IoC容器控制之外创建的对象。请看"使用Spring的AspectJ来依赖注入领域对象"。 下面的例子展示了基于XML配置元数据的基本结构:
1 | <?xml version="1.0" encoding="UTF-8"?> |
`id`属性是一个你用来识别私有bean定义的字符串。`class`属性定义了bean的类型并且使用了完全限定类型名称(全限定名称或完全限定名)。`id`属性的值引用了协作对象。这个例子的中没有展示如何引用协作对象,更多信息请查看『依赖』。
3.2.2 实例化容器
实例化一个Spring IoC容器是简单的。一个或多个提供给`ApplicationContext`构造函数的定位路径实际上是资源字符串,可以让容器从各种例如局部文件系统,Java的`CLASSPATH`等外部资源中加载配置元数据。
1 | ApplicationContext context = |
在你学习Spring IoC容器之后,你可能想知道更多关于Spring的
Resource
抽象信息,像『第四章 资源』描述的那样,Resource
抽象提供了一种从URI语法定义的位置中读取输入流的方便机制。Resource
路径通常被用来构建应用程序上下文,正如4.7 小节『应用上下文和资源路径』描述的那样。
下面的例子是服务层对象(`services.xml`)的配置文件:
1 | <?xml version="1.0" encoding="UTF-8"?> |
下面的例子是数据访问对象`daos.xml`的配置文件:
1 | <?xml version="1.0" encoding="UTF-8"?> |
在之前的例子中,服务层包括类`PetStoreServiceImpl`和两个类型为`JpaAccountDao`和`JpaItemDao`数据访问对象(基于JPA对象/关系映射标准)。`property name`元素指的是JavaBean属性的名称,`ref`元素指的是另一个bean定义的名称。`id`和`ref`之间的连接表明了协作对象之间的关系。配置对象依赖的更详细信息请看『依赖』。
创建基于XML的元数据
bean定义跨越多个XML文件是非常有用的。通常每一个独立的XML配置文件表示你架构中的一个逻辑层或模块。
在上面的例子中,外部bean定义从`services.xml`、`messageSource.xml`和`themeSource.xml`三个文件中加载。所有位置路径都是相对于进行导入的定义文件的,因此`services.xml`必须跟进行导入的文件在同一个目录下或同一个classpath位置下。如你所见,忽略了最前面的反斜杠,但给定的这些路径是相对的,最好是一点都不使用反斜杠。包括顶层的`<beans/>`元素在内,被导入的文件内容必须是依据Spring Schema有效的XML bean定义。
在父目录的引用文件使用”../“相对路径是可以的,但不推荐这样做。这样做会产生一个当前应用之外文件依赖。引用文件特别不推荐在”classpath:” URLs中(例如”classpath:../services.xml”),运行时解析处理会选择”最近的”classpath根目录,然后去寻找它的父目录。Classpath配置的更改可能会导致进入一个不同且不正确的目录。
你可以总是使用完全限定资源位置代替相对路径:例如,”file:C:/config/services.xml”或”classpath:/config/services.xml”。但是要注意你正在将你的应用配置与特定的绝对路径耦合。通常更可取的方式是间接的访问绝对路径,例如,通过”${…}”占位符在运行时解析JVM系统属性。
3.2.3 使用容器
`ApplicationContext`是一个更高级的工厂接口,它能维护不同beans及其依赖的注册表。使用方法`T getBean(String name, Class<T> requiredType)`你可以取回你的beans实例。
`ApplicationContext`能让你用下面的方式读取bean定义及访问它们:
1 | // create and configure beans |
你可以用`getBean()`取回你的beans实例。`ApplicationContext`接口有一些其它的方法来取回beans,但理想的应用代码应该绝不使用它们。事实上,你的应用代码应该完全不调用`getBean()`方法,因此完全不依赖Spring APIs。例如,Spring的集成web框架提供了各种web框架类的依赖注入,例如控制器和JSF管理的beans。
3.3 Bean概述
Spring IoC容器管理一个或多个beans。这些beans由提供给容器的配置元数据生成,例如,XML形式的<bean/>
定义。
在容器本身内部,这些bean定义被表示成BeanDefinition
对象,含有以下元数据:
- 包限定的类名:通常是被定义的bean的实现类。
- bean行为配置元素,规定了bean在容器中的行为(作用范围、生命周期回调函数)等等。
- bean工作需要的引用的其它bean,这些引用也被称为协作者或依赖。
- 其它的配置在新创建的对象中设置,例如,bean中使用的连接数量控制着一个连接池,或连接池的大小限制。
元数据转化为一系列的属性,这些属性构成了每个bean的定义。
表3.1. bean定义
Property | Explained in… |
---|---|
class | Section 3.3.2, “Instantiating beans” |
name | Section 3.3.1, “Naming beans” |
scope | Section 3.5, “Bean scopes” |
constructor arguments | Section 3.4.1, “Dependency Injection” |
properties | Section 3.4.1, “Dependency Injection” |
autowiring mode | Section 3.4.5, “Autowiring collaborators” |
lazy-initialization mode | Section 3.4.4, “Lazy-initialized beans” |
initialization method | the section called “Initialization callbacks” |
destruction method | the section called “Destruction callbacks” |
除了bean定义中包含怎么创建一个指定的bean的信息之外,ApplicationContext
实现也允许用户注册容器之外创建的现有对象。这是通过调用ApplicationContext’s BeanFactory的getBeanFactory()方法完成的,这个方法会返回BeanFactory的实现类DefaultListableBeanFactory
。DefaultListableBeanFactory
支持通过registerSingleton(..)
和registerBeanDefinition(..)
方法来注册。不管怎样,标准应用仅使用通过元数据bean定义定义的beans。
> bean元数据和人工提供的单例需要尽可能早的进行注册,为了使容器在自动注入及其它的内省步骤时能恰当的推理它们。虽然在一定程度上是支持覆盖现有的元数据和单例的,但运行时新beans的注册(并发实时访问工厂)是不被正式支持的,可能会引起并发访问异常,在容器中的与/或状态不一致。
3.3.1 beans命名
每个bean都有一个或多个标识符。这些托管bean的标识符在容器中必须是唯一的。一个bean通常只有一个标识符,但如果一个bean需要不止一个标识符,其它的标识符会被当成别名。
在基于XML的配置元数据中,你可以使用id
和/或name
属性指定bean标识符。id
属性允许你指定一个确定的id。按照惯例这些名字是字母数字的(‘myBean’, ‘fooService’等等),但也可能包含指定字符。如果你想引入bean其它的别名,你可以在name
属性中指定别名,用逗号 (,
),分号(;
),或空格分开。作为一个历史注解,在之前的Spring 3.1版本,id
属性被定义为一种xsd:ID
类型,可以通过合理字符来约束(XML控制id
唯一性)。从Spring 3.1开始,它被定义为xsd:string
类型。注意bean id
的唯一性仍然是容器强制的,虽然不再通过XML解析器来控制(容器控制id
唯一性)。
Bean命名规范
当命名bean时,采用的规范是标准Java实例字段命名规范。bean名称以小写字母开头,采用驼峰式的命名规则。这种命名方式的例子(不带引号)有’accountManager’, ‘accountService’, ‘userDao’, ‘loginController’等等。
一致的命名beans可以使人更容易读懂和理解你的配置,如果你正在使用Spring AOP,使用一致性来命名一系列bean名称是非常有帮助的。
在classpath中进行组件扫描,Spring会根据上面的规则为未命名组件产生bean名称,本质上来说,是采用简单的类名并将其首字母改成小写。然而在特殊情况下(不平常的),当类名有不止一个字母且第一二个字母都是大写的情况下,会保留最初始的状态。与
java.beans.Introspector.decapitalize
定义中的规则是相同的(Spring也采用这个规则)。
为bean定义别名
在定义bean时,通过与id
属性指定的名称相结合,你可以为bean提供不止一个名字,在name
属性中定义任何数量的其它名字。这些名字是同一个bean的等价别名,在一些情况下是非常有用的,例如允许应用中的每个组件通过bean名称引用一个共通的依赖,这个依赖为每个组件本身指定了一个名称。
然而在bean实际定义的地方指定所有别名并不总是适当的。有时会要求引入一个在别的地方定义的bean的别名。这通常是在大的系统中而配置被分割在每个子系统中,每个子系统有它知道对象定义集合。在基于XML配置元数据中,你可以使用<alias/>
来完成别名的定义。
1 | <alias name="fromName" alias="toName"/> |
在这种情况下,在同一个容器中的bean被命名为fromName
,也可能是在别名定义使用之后,被作为toName
引用。
例如,子系统A的配置元数据可能通过名称subsystemA-dataSource
引用数据源。子系统B的配置元数据可能通过名称subsystemB-dataSource
引用数据源。当构成主应用的时,主应用使用这些子系统并通过名称myApp-dataSource
引用数据源。为了使这三个名称引用同一个对象,你可以将如下的别名定义添加到MyApp配置元数据中:
1 | <alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/> |
现在主应用和每个组件都能通过名称引用数据源,这个名称是唯一的且能保证不与任何其它的定义相冲突(有效的创建了一个命名空间),但它们引用了同一个bean。
Java配置
如果你正在使用Java配置,
@Bean
注解可以用来提供别名,更多细节请看3.12.3小节, “使用@Bean注解”。
3.3.2 beans实例化
bean定义本质上来说是创建一个或多个对象的方法。当问及一个命名bean时,容器会查看这个方法并使用bean定义中封装的配置元数据创建(或取得)一个实际的对象。
如果你使用基于XML的配置元数据,你可以指定对象的类型(或类),它将在<bean/>
元素中的class
属性中进行实例化。class
属性,在BeanDefinition
实例的内部是Class
性质,通常是必需的。(例外的情况,请看”使用实例工厂方法进行实例化”小节和3.7小节,”bean定义继承”)。你可以通过以下两种方式中的一种使用Class
属性:
通常情况下,指定要构造的bean类,容器本身通过反射调用bean的构造方法直接创建bean,这与Java代码中使用
new
操作符是等价的。在不常见的情况下,指定包含静态工厂方法的实际类,调用静态工厂方法创建对象,容器在类上调用静态工厂方法创建bean。静态工厂方法调用返回的对象类型可能是同一个类,也可能完全是另一个类。
内部类命名 如果你想为静态嵌套类配置bean定义,你必须使用嵌套类的二进制名字。
例如,如果你在
com.example
包中有个类叫Foo
,Foo
类中有一个静态嵌套类叫Bar
,'class'
属性在bean定义中的值为
com.example.Foo$Bar
注意名字中
$
符号的使用是为了将外部类名与嵌套类名分隔开。
使用构造函数实例化
当你使用构造方法创建bean时,所有的正常类都可以被Spring使用和兼容。也就是说,正在进行开发的类不需要实现任何特定的接口或以特定的方式进行编码。简单的指定bean类就足够了。然而,根据你为指定的bean所使用的IoC类型,你可能需要一个默认的(空的)构造函数。
事实上,Spring的IoC容器可以管理任何你想让它管理的类;它不受限于管理真实的JavaBeans。大多数Spring用户更喜欢实际的JavaBeans,在容器中它仅有一个默认(无参)的构造函数,并且属性之后有合适的setters,getters方法。在容器中你也可以有更多外来的非bean类型的类。例如,如果你需要使用遗留的连接池,这绝对不符合JavaBean规范,但Spring也可以管理它。
基于XML的配置元数据你可以用如下方式指定你的bean的类:
1 | <bean id="exampleBean" class="examples.ExampleBean"/> |
更多关于为构造函数提供参数(如果有必要的话)的机制和构造对象之后设置对象实例属性的细节,请看依赖注入。
通过静态工厂方法进行实例化
当定义的bean用静态工厂方法创建时,你可以使用class
属性指定包含静态工厂方法的类,用factory-method
属性指定工厂方法本身的名字。你应该能调用这个方法(用后面描述的可选参数)并且返回一个实时对象,随后对这个对象进行处理,就好像这个对象是通过构造函数创建的一样。这种bean定义的一个用法是在遗留代码(旧代码)中调用静态工厂方法。
下面的bean定义指定了一个通过调用工厂方法创建的bean。定义没有指定返回对象的类型只有包含工厂方法的类。在这个例子中,createInstance()
必须是一个静态方法。
1 | <bean id="clientService" |
1 | public class ClientService { |
更多关于为工厂方法提供(可选)参数的原理和从工厂方法返回对象后设置对象实例属性的信息,请看”依赖和详细配置”。
通过实例工厂方法进行实例化
与通过静态工厂方法进行实例化类似,通过实例化工厂方法进行实例化,要从容器中调用现有bean非静态方法创建一个新的bean。使用这种机制,要让class
属性为空,在factory-bean
属性中,在包含实例化方法的当前容器(或父/祖先)中指定bean的名字,通过调用实例化方法来创建对象。通过factory-method
属性设置工厂方法本身的名字。
1 | <!-- the factory bean, which contains a method called createInstance() --> |
1 | public class DefaultServiceLocator { |
一个工厂类可以拥有多个工厂方法,如下所示:
1 | <bean id="serviceLocator" class="examples.DefaultServiceLocator"> |
1 | public class DefaultServiceLocator { |
这个方法展示了工厂bean本身可以通过依赖注入(DI)来管理和配置。更多细节请看”依赖和配置”。
在Spring文档中,工厂bean引用了配置在Spring容器中的bean,Spring容器将通过实例或静态工厂方法来创建对象。相比之下,
FactoryBean
(注意大写)引用了Spring特定的FactoryBean
。
3.4 依赖
标准企业应用不会由一个对象(或Spring用语中的bean)组成。即使是最简单的应用也是由一些对象共同工作,呈现给终端用户用户看到的是一个连贯的应用。接下来的一节阐述了如何从定义许多独立的bean定义到完全实现的应用,它是一个通过对象协作来实现目标的过程。
3.4.1 依赖注入
依赖注入(DI)是一个处理过程,凭借对象之间依赖关系,也就是和它们一起工作的其它对象,只能通过构造函数参数,传递参数给工厂方法,在构造完成或工厂方法返回的对象实例之后再设置对象实例的属性。当创建bean时容器再将这些依赖对象注入进去。这个过程从根本上颠倒了bean本身通过直接构建类或通过一种机制例如服务定位模式来控制依赖对象的实例化或定位,因此命名为控制反转(IoC)。
使用依赖注入原则会使代码更简洁,当对象由依赖关系提供时解耦更有效。对象不会查找它的依赖,不知道依赖的位置和依赖关系的类别。同样的,你的类也变的更容易测试,尤其是依赖关系在接口或抽象基类之间的时候,这种情况下单元测试中会要求存桩或模拟实现。(注:Stub和Mock都是软件测试中使用的东西,如有疑问请自行google或百度)。
依赖有两个主要变种,基于构造函数的依赖注入和基于Setter的依赖注入。
基于构造函数的依赖注入
基于构造函数的依赖注入通过容器调用有参数的构造函数来实现,每个参数表示一个依赖。调用指定参数的静态工厂方法来构造bean是近似等价的,这里的讨论将给构造函数和静态工厂方法传参看成是类似的。接下来的例子展示了一个类仅能通过构建函数注入进行依赖注入。注意这个类没什么特别的,它是一个POJO,不依赖于容器特定的接口,基类或注解。
1 | public class SimpleMovieLister { |
构造函数参数解析
构造函数参数解析使用参数类型进行匹配。如果bean定义的构造函数参数中不存在潜在的歧义,bean定义中定义构造函数参数的顺序为bean实例化时提供给恰当构造函数的参数顺序。细想下面的类:
1 | package x.y; |
不存在潜在的歧义,假设Bar
类和Baz
类之间不存在继承关系。因此下面的配置会工作良好,你不必在<constructor-arg/>
元素中显式的指定构造函数参数索引的与/或类型。
1 | <beans> |
当引用另一个bean时,类型已知,匹配正确(像上面的例子一样)。当使用简单类型时,例如<value>true</value>
,Spring不能决定值的类型,因此没有帮助不能按类型匹配。考虑下面的例子:
1 | package examples; |
在上面的场景中,如果你用type
属性显式的指定了构造参数的类型,对于简单类型容器可以使用类型匹配。例如:
1 | <bean id="exampleBean" class="examples.ExampleBean"> |
使用index
属性来显式的指定构造函数参数的索引,例如:
1 | <bean id="exampleBean" class="examples.ExampleBean"> |
除了要解析多个简单值的歧义性之外,当构造函数有两个相同类型的的参数时,指定索引可以解决歧义性问题。注意索引是从0开始的。
你也可以使用构造函数参数名字解决值的歧义问题。
1 | <bean id="exampleBean" class="examples.ExampleBean"> |
记住,要使这个起作用你的代码必须使用调试模式进行编译,这样Spring可以从构造函数中查找参数名称。如果你不能用调试模式进行编译(或不想),你可以使用JDK注解@ConstructorProperties
显式的命名你的构造函数参数。样板类如下所示:
1 | package examples; |
基于Setter的依赖注入
基于Setter的依赖注入在容器调用无参构造函数或无参静态工厂方法之后,通过调用bean的setter方法来实现依赖注入。
下面的例子显示了一个类只能通过纯粹的setter注入进行依赖注入。这个类是常见的Java类。它是一个不依赖于容器中特定接口、基类或注解的POJO。
1 | public class SimpleMovieLister { |
ApplicationContext
支持基于构造函数和基于setter对它管理的bean进行依赖注入。它也支持一些依赖通过构造函数方法注入之后,使用基于setter的依赖注入。使用BeanDefinition
形式配置依赖项,结合PropertyEditor
实例可以将属性从一种形式转成另一种形式。然而大多数Spring用户直接使用这些类(例如以编程形式),而使用XML定义bean,注解组件(例如类中使用 @Component
,,@Controller
注解等等),或在基于Java的@Configuration
类使用@Bean
方法。
使用基于构造函数的依赖注入还是基于setter的依赖注入?
你可以混合使用基于构造函数的依赖注入和基于setter的依赖注入,强制依赖使用构造函数注入,可选依赖使用setter方法或配置方法注入是一个很好的经验法则。注意在setter方法上使用
@Required
注解会检查依赖是否注入。当实现的应用组件是不可变对象时,Spring团队通常主张构造函数注入,这样可以确保所需的依赖非空。此外,基于构造函数注入的组件总是以完全初始化状态返回客户(调用)代码。作为附注,含有许多构造函数参数的代码给人的感觉很差,这意味着类可能有很多职责,应该进行重构以便更好的处理关注问题的分离。
setter注入应该主要用来可选依赖上,在类内可以给可选依赖指定合理的默认值。此外,在每处使用依赖的代码都要进行非空检查。setter注入的一个好处就是setter方法使类的对象在后面可以进行再配置或再注入。
JMX MBeans
的管理是setter注入一个非常好的案例。使用依赖注入的类型对于特定的类是最有意义的。有时候,当处理没有源码的第三方类时,使用哪种方式取决于你。例如,如果第三方库没有提供任何setter方法,构造函数注入可能是依赖注入唯一可行的方式。
依赖解析过程
容器按下面的过程处理bean依赖解析:
创建
ApplicationContext
并使用描述所有bean的配置元数据初始化ApplicationContext
,配置元数据可以通过XML,Java代码或注解指定。对于每一个bean,它的依赖通过属性、构造函数参数、或静态工厂方法参数的形式表示,静态工厂方法可以替代标准的构造函数。当bean在实际创建时,这些依赖会提供给bean。
每个属性或构造函数参数或者是根据实际定义设置的值,或者是容器中另一个bean的引用。
每个属性或构造函数参数是一个从指定形式转成实际类型的属性或构造函数参数的值。
当容器创建后Spring容器会验证每个bean的配置。然而,bean属性本身只有bean创建时才会进行设置。bean是单例的并且当容器创建时会进行提前实例化(默认情况)。作用范围是在3.5 小节”Bean scopes”中定义的。此外,只有需要时候才会创建bean。bean的创建可能会引起beans图的创建,当bean的依赖和它的依赖的依赖(等等)创建和指定的时候。注意这些依赖中解析不匹配可能会在后面出现,例如,受影响的bean第一次创建时。
循环依赖
如果你主要使用构造函数注入,有可能会出现一个不能解决的循环依赖状况。
例如,类A需要通过构造函数注入得到一个类B的实例,而类B需要通过构造函数获得一个类A的实例。如果你为类A和类B配置了互相注入的bean,Spring IoC容器在运行时检测到循环引用,会抛出
BeanCurrentlyInCreationException
。一个可能的解决方案是编译某个类的源代码使其通过setter注入而不是构造函数注入。或者,避免构造函数注入仅用setter注入。换句话说,尽管是不被推荐的,但你可以通过setter注入配置循环依赖。
不像通常的情况(没有循环依赖),bean A和bean B之间的循环依赖可以强制其中的一个bean优先注入另一个bean中,可以使其完全初始化(古老的鸡/蛋场景)。
通常情况下你可以信任Spring去做正确的事情。在容器加载时它检测配置问题,例如引用不存在的beans和循环依赖。当bean实际创建时,Spring设置属性和解析依赖尽可能的晚。这意味着Spring容器正确加载但后面可能会产生异常,当你请求一个对象时,创建对象或它的某个依赖时出现问题,这时容器就会抛出异常。例如,由于缺失或存在无效属性,bean会抛出异常。在真正需要这些beans之前创建它们,会花费一些前期时间和内存,当ApplicationContext
创建时你会发现配置问题,而不是在创建之后。为了单例bean懒惰初始化而不是预先实例化,你仍需要重写这个默认行为。
如果没有循环依赖存在,当一个或更多协作beans注入到一个独立的bean中,在注入独立bean之前,每个协作bean都是完全配置的。这意味着如果bean A有个依赖为bean B,Spring IoC容器在调用bean A的setter方法之前会完整的配置bean B。换句话说,bean被实例化(不是预先实例化的单例),设置依赖和相关的生命周期方法(例如配置初始化方法或初始化bean回调方法)被调用。
依赖注入的例子
下面的例子使用基于XML的配置元数据进行setter注入。Spring XML配置文件中的一小部分指定了一些bean的定义:
1 | <bean id="exampleBean" class="examples.ExampleBean"> |
1 | public class ExampleBean { |
在上面的例子中,setter声明匹配XML文件中指定的属性。下面的例子使用了基于构造函数的依赖注入:
1 | <bean id="exampleBean" class="examples.ExampleBean"> |
1 | public class ExampleBean { |
bean定义中指定的构造函数参数将作为ExampleBean
的构造函数参数使用。
现在考虑这个例子的一个变种,不使用构造函数,而是Spring调用静态工厂方法返回对象的一个实例:
1 | <bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance"> |
1 | public class ExampleBean { |
静态工厂方法的参数通过<constructor-arg/>
元素提供,与构造函数使用的完全一样。虽然这个例子中工厂方法返回值的类型与包含静态工厂方法的类的类型一样,但它们可以不一样。工厂方法的实例(非静态)的使用本质上样式完全一样(除了使用factory-bean
属性代替class
属性之外),因此这儿不讨论这些细节。
3.4.2 依赖和配置的细节
正如上一节提到的那样,你可以通过引用其它被管理bean(协作者)来定义bean的属性和构造函数参数,或者在行内定义值。为了实现这个功能,Spring的基于XML的配置元数据在它的<property/>
和<constructor-arg/>
中支持子元素类型。
直接使用值 (基本类型,字符串等等)
<property/>
元素的value
属性指定了一个属性或构造函数参数作为可读的字符串表示。使用Spring的转换服务将这些值从String
转成属性或参数的真实类型。
1 | <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> |
下面的例子为了更简洁的XML配置使用了p命名空间.
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
上面的XML是更简洁的;然而,错别字是在运行时发现而不是在设计时,除非你使用IDE例如IntelliJ IDEA
或Spring Tool Suite (STS)
,当你创建bean定义时它们支持自动的属性补全。IDE辅助是强烈推荐的。
你也可以配置java.util.Properties
实例:
1 | <bean id="mappings" |
Spring容器通过JavaBeans的PropertyEditor
机制将<value/>
元素内部的文本转成java.util.Properties
实例。这是一个很好的捷径,使用嵌入的<value/>
元素而不是使用value
属性的方式,是Spring团队支持的几个地方之一。
idref元素
在容器中传递另一个bean的id(字符串值,不是引用)到<constructor-arg/>
或<property/>
元素时,idref
元素是一种简单的的误差检验方式。
1 | <bean id="theTargetBean" class="..."/> |
上面的bean定义片段与下面的代码片段是等价的(运行时):
1 | <bean id="theTargetBean" class="..." /> |
第一种形式优于第二种形式,因为idref
标签允许容器在部署时验证引用的bean,即命名的bean实际存在。在第二种形式中,当值传给client
的targetName
时没有进行验证。拼写错误只有在client
bean实际创建时才会发现(最可能有严重后果)。如果client
bean是原型bean,拼写错误和产生的异常可能只有在容器部署很长时间之后才会发现。
idref
元素的local
属性在4.0 beans xsd中不再支持,因为它不再为合格的bean引用提供值。简单将你现有的idref local
引用改成idref bean
当更新到4.0 schema时。
<idref/>
元素带来值的通常位置(至少在Spring 2.0之前)是在ProxyFactoryBean
bean定义中的AOP拦截器配置中。当你指定拦截器名字时使用<idref/>
元素来防止误拼拦截器id。
其它bean的应用(协作bean)
ref
元素是<constructor-arg/>
或<property/>
定义元素的最终的元素。在这个元素中设置bean的指定属性的值,值为容器管理的另一个bean(协作bean)的引用。引用的bean是设置属性bean的依赖,在属性设置之前引用bean需要进行初始化。(如果协作bean是一个单例模式的bean,它可能已经被容器初始化了。)所有引用bean根本上都是另一个对象的引用。作用域和验证是根据你是否通过bean
,local
,或parent
属性指定了另一个对象的id/name来决定的。
通过<ref/>
标签的bean
属性指定目标bean是最常用的形式,允许创建同容器或父容器中任何bean的引用,不管它是否是在同一个XML文件中。bean
属性的值可能与目标bean的id
属性值相同,或与目标bean的name
属性值相同。
1 | <ref bean="someBean"/> |
通过parent
属性指定目标bean会引用当前容器的父容器中的bean。parent
属性的值可能与目标bean的id
值或name
值相同,目标bean必须在当前容器的父容器中。当你有一个容器分层的时候你可以使用parent
,你想将现有bean包裹在有代理的父容器中且现有bean与父容器中的bean同名,你可以使用parent
属性。
1 | <!-- in the parent context --> |
1 | <!-- in the child (descendant) context --> |
idref
元素的local
属性在4.0 beans xsd中不再支持,因为它不再为合格的bean引用提供值。简单将你现有的idref local
引用改成idref bean
当更新到4.0 schema时。
内部bean
<property/>
或<constructor-arg/>
元素内的<bean/>
元素中定义bean称为内部bean。
1 | <bean id="outer" class="..."> |
内部bean定义不要求定义id或name;如果指定了,容器不用用这个值作为标识符。容器创建时也忽略scope
标记:内部bean总是匿名的且它们总是由外部bean创建。除了注入到封闭bean中或独立的访问它们,不可能将内部bean注入到协作bean中。
作为一种很少出现的情况,从特定的域中有可能会收到销毁回调函数,例如,对于请求域内的内部bean包含单例bean:内部bean实例的创建会绑定到它的包含bean,但销毁回调函数允许它进入到请求域的生命周期中。这不是一个常见的场景;内部bean通常简单的共享它们的包含bean的作用域。
集合
在<list/>
,<set/>
,<map/>
和<props/>
元素中,你要分别设置Java Collection
类型list
,set
,map
和Properties
的属性和参数。
1 | <bean id="moreComplexObject" class="example.ComplexObject"> |
map的key或value,或者是set value的值也可以是下面元素中的任何一个:
1 | bean | ref | idref | list | set | map | props | value | null |
集合合并
Spring也支持集合的合并。应用开发者可以定义父类型<list/>
,<map/>
,<set/>
或<props/>
元素,可以有继承和覆盖父集合的子类型元素<list/>
,<map/>
,<set/>
或<props/>
。也就是说,子集合的值是父集合和子集合中元素合并的结果,子集合元素覆盖了父集合元素的值。
关于合并的这节讨论了父子bean机制。对父子bean定义不熟悉的读者可以去读相关的章节。
下面的例子示范了集合合并:
1 | <beans> |
注意child
bean定义中的adminEmails
属性下的<props/>
元素使用了merge=true
属性。当容器解析并实例化child
bean时,最终的实例含有adminEmails
Properties
集合,集合中的值是子adminEmails
集合和父adminEmails
集合合并的结果。
1 | administrator=administrator@example.com |
子Properties
集合的值继承了父<props/>
中的所有属性元素,子集合中的support
值覆盖了父集合中的值。
<list/>
,<map/>
和<set/>
集合类型中的合并与上面类似。在特定的<list/>
元素情况下,关于List
集合类型的语义,也就是说,有序集合值的概念仍然是保留的;父list中的值领先于所有子list中的值。在Map
,Set
和Properties
集合类型,不存在顺序。因此,无序语义在容器内部使用的集合类型Map
,Set
和Properties
的实现基础上是有效的。
集合合并的限制
你不能合并不同的集合类型(例如Map
和List
),如果你试图合并不同的集合类型会有适当的抛出Exception
。merge
属性必须在更低的、继承的子定义中;在父集合定义中指定merge
属性是多余的并且不会进行合并。
强类型集合
随着Java 5中泛型的引入,你可以使用强类型集合。也就是说,你可以声明一个Collection
类型但它只能包含String
元素(例子)。如果你使用Spring将一个强类型的Collection
注入到bean中,你可以利用Spring的类型转换支持,例如在将元素添加到Collection
之前,将你的强类型Collection
实例中的元素转成恰当的类型。
1 | public class Foo { |
1 | <beans> |
当注入foo
bean的accounts
属性时,强类型Map<String, Float>
中元素类型的泛型信息可以通过反射得到。因此Spring的类型转换结构能识别各种值元素的类型为Float
,字符串9.99, 2.75
和3.99
会被转换成实际的Float
类型。
Null和空字符串
Spring把属性的空参数都处理为空Strings
。下面基于XML的配置元数据片段将email属性设为空String
值(“”)。
1 | <bean class="ExampleBean"> |
上面的例子与下面的Java代码是等价的:
1 | exampleBean.setEmail("") |
<null/>
元素处理null
值。例如:
1 | <bean class="ExampleBean"> |
上面的配置与下面的Java代码等价。
1 | exampleBean.setEmail(null) |
XML 使用p命名空间的缩写
p命名空间可以让你不需要嵌入<property/>
元素便能使用bean
元素的属性来描述你的属性值以及/或协作beans。
Spring支持含有命名空间的扩展配置形式,命名控件是基于XML Schema定义的。本章讨论的beans配置形式是在XML Schema文档中定义的。但是p命名空间不能在XSD文件中定义并且只在Spring core中存在。
下面的例子显示了两个XML片段,解析结果是相同的:第一个是标准的XML形式,第二个使用了p命名空间。
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
这个例子显示了bean定义中p命名空间中有个一个叫email的属性。这会通知Spring包含属性声明。如前面所述,p命名空间没有schema定义,因此你可以将特性值(attribute)设到属性值(property)上。
下面的例子包括两个bean定义,且它们都引用了另一个bean:
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
正如你所看到的,这个例子不仅包括使用了p命名空间的属性值,而且使用了一种特定的形式来声明属性引用。然而第一个bean定义使用<property name="spouse" ref="jane"/>
创建了一个从bean john
到bean jane
的引用,第二个bean定义使用p:spouse-ref="jane"
作为一个特性同样定义了从bean john
到bean jane
的引用。在spouse
是属性名的情况下,-ref
部分表示这不是一个直接的值而是另一个bean的引用。
p命名空间不是标准的XML格式,例如,声明的属性引用会与以
Ref
结尾的属性相冲突,而标准XML格式则不会。我们建议你仔细的选择你的方法并与你的团队成员交流,避免生成的XML文档同时使用了三种方式。
XML 使用c命名空间的缩写
与“XML shortcut with the p-namespace”小节类似,在Spring 3.1新引入的c命名空间允许使用行内属性配置构造函数参数而不用嵌入constructor-arg
元素。
让我们重新回顾一下“Constructor-based dependency injection”小节中的例子并使用c:
命名空间:
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
c:
命名空间遵循与p:
命名空间相同的约定在通过名字设置构造函数参数时。同样的,它也需要进行声明,虽然它不能在XSD schema中使用(但在Spring core中存在)。
对于很少出现的不能找到构造函数参数名字的情况(通常如果编译字节码且没有调试信息),可以使用参数索引:
1 | <!-- c-namespace index declaration --> |
由于XML语法,索引符号需要前面加上
_
,因为XML属性名字不能以数字开头(即使一些IDE允许)。
在实践中,构造函数解析机制能有效匹配参数,因此除非真的需要,否则我们推荐在配置中使用名字符号。
混合属性名字
当你设置bean属性时,你可以使用混合的或嵌入的属性名字,只要路径中除了最后的属性名之外所有组件都是非null
。考虑下面的bean定义。
1 | <bean id="foo" class="foo.Bar"> |
foo
bean有一个fred
属性,fred
有一个sammy
属性,bob
有一个sammy
属性,最后的sammy
属性设置值为123
。为了这样设置,foo
的fred
属性,fred
的bob
属性在bean创建后必须是非null
或抛出NullPointerException
。
3.4.3 使用depends-on
如果一个bean是另一个bean的一个依赖,这通常意味着一个bean作为另一个bean的一个属性去设置。在基于XML的配置元数据中通常使用<ref/>
元素实现。然而有时beans之间的依赖关系是间接的;例如,类中的静态初始化程序需要触发,例如数据驱动注册。depends-on
特性能显示的强制一个bean或多个beans在使用这个元素的bean初始化之前进行初始化。下面的例子使用depends-on
特性表示一个单一bean的一个依赖:
1 | <bean id="beanOne" class="ExampleBean" depends-on="manager"/> |
为了表示多个bean上的依赖关系,提供一个bean名字列表作为depends-on
特性的值,用逗号,空格或分号作为有效分隔符:
1 | <bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao"> |
depends-on
特性在bean定义中可以指定初始化时的依赖和对应的销毁时依赖(仅在单例情况下)。依赖beans与给定bean之间定义了一个depends-on
关系,依赖beans在给定bean本身被销毁之前首先被销毁。因此depends-on
也可以控制销毁顺序。
3.4.4 延迟初始化beans
默认情况下,作为初始化过程的一部分,ApplicationContext
实现时渴望创建并配置所有的单例beans。通常情况下,预实例化是必要的,因为配置中或周围环境中的错误可以立即发现,与几小时或几天后发现截然相反。当预实例化是不必要的时候,你可通过标记bean定义为延迟初始化来阻止单例bean的预实例化。延迟初始化的bean会通知IoC容器当第一次请求bean时创建一个bean实例,而不是在启动时创建。
在XML中,延迟初始化通过<bean/>
元素中的lazy-init
特性来控制;例如:
1 | <bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/> |
当ApplicationContext
读取到上面的配置,ApplicationContext
启动时名字为lazy
的bean不会进行预实例化,而名字为not.lazy
的bean会进行预实例化。
然而,当延迟初始化的bean是一个非延迟初始化的单例bean的依赖时,ApplicationContext
会在启动时创建延迟初始化的bean,因为它必须提供单例bean的依赖。延迟初始化的bean会注入到单例bean中,而在其它地方它是非延迟初始化的。
你也可以在容器中通过<beans/>
中的default-lazy-init
特性控制延迟初始化;例如:
1 | <beans default-lazy-init="true"> |
3.4.5 自动注入协作bean
Spring容器能自动装配协作beans之间的关联关系。你可以允许Spring通过检查ApplicationContext
中的内容自动的为你的bean解析协作者(其它bean)。自动装配有以下优势:
自动装配能明显减少指定属性或构造函数参数的需要。(其它的机制例如在本章其它地方讨论的bean模板在这一点上也是非常重要的。)
当对象变化时自动装配能更新配置。例如,如果你需要增加一个类的依赖项,依赖项可以是满足自动装配的而不需要你去修改配置。因此自动装配在开发时尤其有用,当代码基础变的更稳定时可以改为显式装配。
当使用基于XML的配置元数据时,通过使用<bean/>
元素的autowire
特性你可以指定一个bean定义的自动装配模式。自动注入功能有四种模式。你可以指定每个bean的自动装配模式,因此你可以选择使用哪一种模式。
表 3.2 自动装配模式
模式 | 解析 |
---|---|
no | (默认)无自动装配。引用bean必须通过ref 元素定义。对于更大的部署,不推荐更改默认设置,因为显式指定协作者更清晰并且更易控制。在某种程度上来说,它记录了系统的结构。 |
byName | 通过属性名称自动装配。Spring寻找与需要自动装配的属性同名的bean。例如,如果一个bean定义设置为通过名称自动装配,它有一个master 属性(也就是说,它有一个setMaster(..) 方法),Spring寻找名字为master 的bean定义,使用它设置属性值。 |
byType | 如果容器中含有属性类型已知的一个bean,那么可以允许按类型自动装配属性。如果此类型的bean不止一个,则会抛出致命的异常,这意味着你可能不能使用byType 来注入那个bean。如果没有匹配的bean,则什么也不做;属性没有被设置。 |
constructor | 与byType 类似,但是应用到构造函数参数上的。如果容器中没有一个构造函数参数bean的确定类型,将会抛出一个致命的异常。 |
通过byType
或构造函数自动装配模式,你可以配置数组和集合类型。在这种情况下容器内所有能匹配期望类型的自动装配候选对象将被提供合适的依赖项。如果期望的key
类型是String
类型,你可以自动装配强类型的Maps
。自动装配的Maps
的值将有所有匹配期望类型的bean组成,Maps
的键将包含对应的bean名称。
你可以将依赖检查与自动装配相结合,它将在自动装配完成之后执行。
自动装配的优势与限制
当自动装配在整个工程中一致的使用时其效果最好。如果通常情况下不使用自动装配,仅在一两个bean定义中使用自动装配开发人员可能感到非常困惑。
考虑一下自动装配的限制与缺点:
property
和constructor-arg
中显式依赖的设置总是会覆盖自动装配。你不能自动装配所谓的简单属性例如基本类型,Strings
和Classes
(和简单类型的数组)。这是设计上的限制。与显式配置相比,自动装配是更不确定的。尽管Spring小心的避免猜测以防歧义性引起无法预料的后果,但Spring管理的对象之间的关系不再被显式的记录。
Spring容器中能产生文档的工具可能得不到配置信息。
setter方法或构造函数参数指定的类型进行自动装配时可能匹配到容器中多个bean的定义。对于数组,集合或
Maps
而言,这是一个不必要的问题。然而对于只期望一个值的依赖而言,这个歧义性不能任意解决。如果不能获得唯一的bean定义,会抛出异常。
后面的方案中,你有一些选择:
放弃自动装配支持显式配置。
通过设置bean的
autowire-candidate
特性为false
来避免自动装配。通过设置
<bean/>
元素的primary
特性为true
来指定一个单例bean定义作为主要的候选bean。通过基于注解的配置实现更多细颗粒的控制,如3.9小节 “基于注解的容器配置”。
排除bean在自动装配之外
在单个bean的基础上,你可以排除bean在自动装配之外。在Spring的XML形式中,设置<bean/>
元素的autowire-candidate
特性为false
;容器会使自动装配基础框架不能得到指定bean定义(包括注解类型的配置,例如@Autowired
)。
你也可以根据bean名称的匹配模式限制自动装配的候选目标。顶层的<beans/>
元素可以接收default-autowire-candidates
特性中的一个或多个模式。例如,为了限制自动装配候选目标匹配任何名字以Repository
结尾的bean,可以提供一个*Repository
值。为了提供多种模式,可以定义一个以逗号为分隔符的列表。bean定义中autowire-candidate
特性显示的值true
或false
最是优先起作用的,对于这些bean而言,模式匹配规则不起作用。
这些技术对于那些你从不想通过自动装配方式注入到其它bean中的beans而言是很有用的。这不意味着一个排除的bean它本身不能通过自动装配进行配置。更确切的说,bean本身不是一个进行其它bean进行自动装配的候选者。
3.4.6 方法注入
在大多数应用场景中,容器中的大多数bean是单例的。当一个单例bean需要与另一个单例bean协作时,或一个非单例bean需要与另一个非单例bean协作时,你通常通过定义一个bean作为另一个bean的一个属性来处理这个依赖关系。当bean的生命周期不同时问题就出现了。假设一个单例bean A需要使用非单例(标准)bean B时,也许A中的每一个方法调用都要使用bean B。容器仅创建单例bean A一次,因此仅有一次设置属性的机会。容器不能在每次需要bean B时提供一个bean B的新的实例。
一个解决方案是放弃一些控制反转。你可以使bean A通过实现ApplicationContextAware
接口感知到容器,每个bean A需要的时候就通过getBean("B")
调用向容器请求(通常是新的)一个bean B的实例。下面是这种方法的一个例子:
1 | // a class that uses a stateful Command-style class to perform some processing |
前面所讲的不是让人满意的,因为业务代码能感知并耦合了Spring框架。方法注入,Spring IoC容器的一个有点高级的特性,允许使用一种干净的方式来处理这个案例。
你可以在blog entry中了解更多关于方法注入的动机。
查找方法注入
查找方法注入是容器的一种覆盖其管理的beans中的方法的能力,可以返回容器中另一个命名bean查找结果。查找通常会涉及到一个标准bean,如前一小节中讲的那样。Spring框架实现了查找方法注入,它是通过使用CGLIB库生成的字节码来动态的产生一个覆盖这个方法的子类。
为了使动态子类化起作用,Spring bean容器要进行子类化的类不能是最终的类,要进行重写的方法也不是最终的方法。
单元测试一个含有抽象方法的类需要你自己对这个类进行子类化,并且提供这个抽象方法的
stub
实现。实体方法对于要求获得实体类的组件扫描也是必需的。
一个更关键的限制是查找方法不能与工厂方法一起工作,尤其是在配置类中不能与
@Bean
方法同时起作用,由于那种情况下容器不能控制实例的创建,因此不能在飞速写入中创建一个运行时产生的子类。最后,方法注入的目标对象不能被序列化。
看一下前面代码片中的CommandManager
类,你可以看到Spring容器将会动态的覆盖createCommand()
方法的实现。CommandManager
类不会有任何Spring依赖,重写的例子如下:
1 | package fiona.apple; |
客户类中包含要注入的方法(在这个例子中是CommandManager
),要注入的方法需要下面形式的一个签名:
1 | <public|protected> [abstract] <return-type> theMethodName(no-arguments); |
如果这个方法是抽象的,动态产生的子类会实现这个方法。另外,动态产生的子类会覆盖原来的类中定义的实体方法。例如:
1 | <!-- a stateful bean deployed as a prototype (non-singleton) --> |
无论什么时候识别为commandManager
的bean需要一个command
bean的新实例,它都会调用它的createCommand()
方法。如果真的需要的话,你必须小心的部署command
bean为一个原型。如果它被部署为一个单例,每次都会返回同一个command
实例。
感兴趣的读者可能也会发现
ServiceLocatorFactoryBean
(在org.springframework.beans.factory.config
包中)使用这种方法。ServiceLocatorFactoryBean
中使用的方法与另一个工具类ObjectFactoryCreatingFactoryBean
中的方法类似,但它允许你指定你自己的查找接口,与Spring特定的查找接口相反。这些类的额外信息请查询Java文档。
任意的方法替换
一种比查找方法注入更少使用的形式是用另一种方法实现替换管理的bean中任意方法的能力。用户可以安全跳过本节剩下的部分,直到这个方法真正需要的时候再看。
在基于XML的配置元数据中,对于一个部署的bean,你可以通过replaced-method
元素用另一个方法实现替换现有的方法实现。考虑下面的类,有一个我们想覆盖的computeValue
方法:
1 | public class MyValueCalculator { |
实现了org.springframework.beans.factory.support.MethodReplacer
接口的类提供了一种新的方法定义。
1 | /** |
部署最初的类的bean定义和指定的重写方法如下:
1 | <bean id="myValueCalculator" class="x.y.z.MyValueCalculator"> |
你可以在<replaced-method/>
元素中使用一个或多个包含<arg-type/>
元素来指出要覆盖的方法的方法签名。只有类中进行了方法重载且有多个重载变种的时候,参数的签名才是必需的。为了简便,字符串类型的参数可能是全拼类型名称的一个子串。例如,下面的所有写法都能匹配java.lang.String
:
1 | java.lang.String |
因为参数数目经常是足够区分每个可能的选择的,通过允许定义匹配参数类型的最短字符串类型,这个缩写可以保存许多类型。
3.5 Bean的作用域
当你创建bean定义时,你创建了一个配方用于创建bean定义中定义的类的实例。bean定义是配方的想法是很重要的,因为这意味着对于一个类,你可以根据一个配方创建许多对象实例。
你不仅能管理要插入对象中的的各种依赖和配置值,而且能管理对象的作用域,对象是从特定的bean定义中创建的。这种方法是强大且灵活的,你可以通过配置文件选择你创建的对象的作用域,从而代替Java类级别对象的内置作用域。定义的beans将部署成多种作用域中的一种:开箱即用,Spring框架支持六种作用域,如果你使用感知web的ApplicationContext
,你只可以使用其中的五种作用域。
下面的作用域支持开箱即用。你也可以创建一个定制的作用域。
表 3.3 bean作用域
作用域 | 描述 |
---|---|
singleton | (默认) 每个Spring IoC容器使单个bean定义只能创建一个对象实例。 |
prototype | 单个bean定义可以创建任何数量的对象实例。 |
request | 单个bean定义的创建实例的作用域为单个HTTP request的声明周期;也就是说,每个HTTP request有它自己的根据bean定义创建的实例。只在感知Spring ApplicationContext 的上下文中有效。 |
session | 单个bean定义的创建实例的作用域为HTTP Session 的生命周期. 只在感知Spring ApplicationContext 的上下文中有效。 |
application | 单个bean定义的创建实例的作用域为ServletContext 的生命周期。 只在感知Spring ApplicationContext 的上下文中有效。 |
websocket | 单个bean定义的创建实例的作用域为WebSocket 的生命周期。 只在感知Spring ApplicationContext 的上下文中有效。 |
从Spring 3.0,引入了
thread scope
作用域,但默认情况下是不注册的。更多的信息请看SimpleThreadScope
文档。关于怎么注册thread scope
作用域或任何其它的定制作用域的介绍,请看『Using a custom scope』小节。
3.5.1 单例作用域
单例bean只管理一个共享实例,id匹配bean定义的所有对beans的请求,Spring容器会返回一个特定的bean实例。
换言之,当你定义一个bean定义时,它的作用域为单例,Spring IoC容器会根据bean定义创建一个确定的对象实例。这个单独的实例存储在单例beans的缓存中,接下来的对这个命名bean的所有请求和引用都会返回那个缓存的对象。
Spring中的单例bean概念不同于《设计模式》书中定义的单例模式。设计模式中的单例是对对象的作用域进行硬编码,为的是每个类加载器只能创建一个特定类的实例。Spring单例作用域最好的描述是每个容器每个类。这意味着如果你在单个的Spring容器中为一个特定的类定义了一个bean,Spring只会根据bean定义创建一个类的实例。在Spring中单例作用域是默认的作用域。为了在XML定义一个单例bean,你可以像下面一样写,例如:
1 | <bean id="accountService" class="com.foo.DefaultAccountService"/> |
3.5.2 原型作用域
非单例模式,bean部署采用原型作用域时,每次产生一个特定bean的请求时都会创建一个新的bean实例。也就是说,这个bean会注入到另一个bean中或你可以在容器中通过调用getBean()
方法来请求它。通常,对于所有有状态的beans使用原型作用域,对于无状态的beans使用单例作用域。
下面的图阐述了Spring原型作用域。数据访问对象(DAO)通常是不会配置为原型的,因为一个典型的DAO不会有任何会话状态;对于作者来说很容易重用单例图的核心。
下面的例子在XML中定义一个原型bean:
1 | <bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/> |
与其它作用域相比,Spring不管理原型bean的完整生命周期:容器初始化、配置,另外组装原型对象,并把它传递给客户端,之后不再记录原型实例。因此,虽然不管什么作用域初始化生命周期回调函数都会在所有对象上调用,但是在原型作用域的情况下,不会调用配置的销毁生命周期回调函数。客户端代码必须清理原型作用域的对象并释放原型bean拥有的昂贵资源。为了使Spring容器释放原型bean拥有的资源,尝试使用定制的bean后处理程序,它拥有需要清理的bean的引用。
在有些方面,关于原型作用域,Spring容器的角色像是Java中new
操作符的替代品。所有生命周期的管理必须由客户端处理。(Spring容器中更多关于bean生命周期的细节,请看3.6.1小节,”生命周期回调”)。
3.5.3 含有原型bean依赖的单例bean
当你使用含有原型bean依赖的单例作用域bean时,要意识到依赖解析是在实例化时。因此如果你使用依赖注入将原型作用域的bean注入到单例作用域的bean中时,将会实例化一个新的原型bean并依赖注入到单例bean中。原型bean实例曾经是唯一提供给单例作用域的bean的实例。
假设你想在运行时让单例作用域的bean重复的获得原型作用域bean的新实例。你不能依赖注入原型作用域的bean到你的单例bean中,因为当Spring容器实例化单例bean,解析并注入它的依赖时,注入只发生一次。如果你在运行时不止一次需要原型bean的实例,请看3.4.6小节,”方法注入”。
3.5.4 Request、session、application和 WebSocket作用域
如果你使用感知web的Spring ApplicationContext
实现(例如XmlWebApplicationContext
),request
,session
,application
和websocket
作用域是唯一可用的作用域。如果你通过正规的Spring IoC容器例如ClassPathXmlApplicationContext
来使用这些作用域,会抛出IllegalStateException
异常,投诉使用了一个未知的bean作用域。
web配置初始化
为了支持request
,session
,application
和websocket
标准的bean作用域,在你定义你的bean之前需要进行一些较小的初始化配置。(对于标准作用域singleton
和prototype
,初始化步骤不需要的。)
如果你使用Servlet 2.5的web容器,在Spring的DispatcherServlet
之外处理请求(例如使用JSF或Struts时),你需要注册org.springframework.web.context.request.RequestContextListener
ServletRequestListener
。对于Servlet 3.0+,能通过WebApplicationInitializer
接口以编程方式处理。对于更早的容器,可以在应用程序的web.xml
文件中添加下面的声明来代替:
1 | <web-app> |
如果你的监听器设置有问题,作为一种选择,你可以考虑Spring的RequestContextFilter
。过滤器映射依赖于web应用程序的相关配置,因此你必须适当的更改它。
1 | <web-app> |
DispatcherServlet
,RequestContextListener
和RequestContextFilter
都是在做同样的事,也就是说将HTTP请求对象绑定到服务请求的Thread
上。这使得request作用域和session作用域的beans在更深一层的调用链中是可用的。
Request作用域
考虑下面的bean定义的XML配置:
1 | <bean id="loginAction" class="com.foo.LoginAction" scope="request"/> |
对于每一个HTTP请求,Spring容器通过使用loginAction
定义创建一个新的LoginAction
bean实例。也就是说,loginAction
bean的作用域是在HTTP请求级别的。你可以任意改变创建的实例的内部状态,因为其它的根据loginAction
bean定义创建的实例不会看到这些状态的改变;它们对于每个单独的请求都是独有的。当请求处理完成时,请求作用域的bean被丢弃。
当使用注解驱动的组件或Java配置时,@RequestScope
注解能用来指定一个组件的作用域为request。
1 |
|
Session作用域
考虑下面的bean定义的XML配置:
1 | <bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/> |
对于单个HTTP Session的生命周期,Spring容器通过userPreferences
bean定义创建一个UserPreferences
bean实例。换句话说,userPreferences
bean的有效作用域是HTTP Session级别的。正如request作用域的beans一样,你可以任意改变你想改变的创建的bean实例的内部状态,知道其它的使用根据userPreferences
bean定义创建的HTTP Session实例也不会看到这些内部状态的改变,因为它们对于每个单独的HTTP Session都是独有的。当HTTP Session被最终销毁时,Session作用域的bean也被销毁。
当使用注解驱动的组件或Java配置时,@SessionScope
注解能用来指定一个组件的作用域为session。
1 |
|
Application作用域
考虑下面的bean定义的XML配置:
1 | <bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/> |
对于整个web应用而言,Spring容器根据appPreferences
bean定义只创建一次AppPreferences
bean的新实例。也就是说,appPreferences
bean的作用域是ServletContext
级别的,作为一个正规的ServletContext
特性来存储。这有点类似于Spring的单例bean,但在两个方面是不同的:它对于每个ServletContext
是单例的,而不是每个Spring ApplicationContext
(在任何给定的web应用中可能有几个ApplicationContext
),它是真正显露的,因此作为一个ServletContext
特性是可见的。
当使用注解驱动的组件或Java配置时,@ApplicationScope
注解能用来指定一个组件的作用域为Application。
1 |
|
具有作用域的bean作为依赖项
Spring IoC容器不仅管理对象的实例化,而且管理协作者(或依赖)的绑定。例如,如果你想将一个具有HTTP request作用域的bean注入到另一个具有更长生命周期作用域的bean中,你可能选择注入一个AOP代理来代替具有作用域的bean。也就是说,你需要注入一个代理对象,这个对象能显露与具有作用域的对象相同的接口,但也能从相关的作用域中(例如HTTP request作用域)得到真正的目标对象,能通过委派方法调用到真正的对象。
你也可以在作用域为
singleton
的beans之间使用<aop:scoped-proxy/>
,将通过中间代理的引用进行序列化,因此能通过反序列化重新获得目标的单例bean。当将作用域为
prototype
的bean声明为<aop:scoped-proxy/>
时,每个在共享代理上的方法调用会引起一个新目标实例(调用朝向的)的创建。通过生命周期安全的方式访问更短的作用域中beans,作用域代理也不是唯一的方式。你也可以简单的声明你的注入点(例如,构造函数/setter参数或自动装配领域)为
ObjectFactory<MyTargetBean>
,考虑到每次需要的时候通过getObject()
调用来取得索要的当前实例——没有分别控制实例或储存它。JSR-300变量被称作
Provider
,对于每一次取回尝试使用Provider<MyTargetBean>
声明和对应的get()
调用。关于JSR-330整体的更多细节请看这儿。
下面例子中的配置只有一行,但对于理解它背后的”why”和”how”是重要的。
1 | <?xml version="1.0" encoding="UTF-8"?> |
为了创建这样一个代理,你插入一个子元素<aop:scoped-proxy/>
到具有作用域的bean定义中(看”选择创建的代理类型”小节和38章,基于XML Schema的配置)。为什么bean定义的作用域为request
,session
和定制作用域级别需要<aop:scoped-proxy/>
元素?让我们检查下面的单例bean定义,并将它与你需要定义的前面提到的作用域进行比较(注意下面的userPreferences
bean定义按目前情况是不完全的)。
1 | <bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/> |
在上面的例子中,单例bean userManager
通过引用被注入到具有HTTP Session
作用域的bean userPreferences
中。这的突出点是userManager
bean是单例:每个容器它将确定的被实例化一次,它的依赖(在这个例子中只有一个,userPreferences
bean)也只注入一次。这意味着userManager
bean只能对确定的同一个userPreferences
对象进行操作,也就是最初注入的那个对象。
当将一个短期作用域的bean注入到一个长期作用域的bean中时,这不是你想要的行为,例如将一个具有HTTP Session
作用域的协作bean作为一个依赖注入到一个单例bean中。当然,你需要一个单一的userManager
对象,对于HTTP Session
的生命周期,你需要一个特定的被称为HTTP Session
的userPreferences
对象。因此容器创建了一个与UserPreferences
类暴露相同的公共接口的对象(理想情况下是一个UserPreferences
实例),这个对象能从作用域机制中(HTTP request,Session等)取得真正的UserPreferences
对象。容器将这个代理对象注入到userManager
bean中,userManager
bean不会意识到UserPreferences
引用是一个代理。在这个例子中,当UserManager
实例调用依赖注入的UserPreferences
对象的方法时,它实际上调用的是代理中的一个方法。代理能从HTTP Session
中(在这个例子)取得真正的UserPreferences
对象,将方法调用委托到取得的真正的UserPreferences
对象上。
因此当注入具有request或session作用域的bean到协作对象中时,你需要下面的,正确的,完整的配置:
1 | <bean id="userPreferences" class="com.foo.UserPreferences" scope="session"> |
选择创建的代理类型
当Spring容器为具有<aop:scoped-proxy/>
标记的bean创建代理时,默认情况下,创建一个基于CGLIB的类代理。
CGLIB代理只拦截公有方法调用。在这个代理上不调用非公有方法;它们不能委托给实际作用域目标对象。
作为一种选择,对于这种具有作用域的bean你可以配置Spring容器创建标准JDK基于接口的代理,通过指定<aop:scoped-proxy/>
元素的proxy-target-class
特定的值为false
。使用JDK基于接口的代理意味着在你应用程序类路径中你不需要额外的库来支持这种代理的使用。然而,它也意味着具有作用域的bean的类必须实现至少一个接口,并且注入这个bean的所有协作者必须通过它接口中的一个来引用它。
1 | <!-- DefaultUserPreferences implements the UserPreferences interface --> |
关于选择基于类或基于接口代理的更多细节信心,请看7.6小节,”代理机制”。
3.5.5 定制作用域
bean作用域机制是可扩展的;你可以定义你自己的作用域,甚至重新定义现有的作用域,虽然后者被认为是一种不好的实践,你不能覆盖内置的singleton
作用域和prototype
作用域。
创建一个定制作用域
为了将你的定制作用域集成到Spring容器中,你需要实现org.springframework.beans.factory.config.Scope
接口,这一节将描述这个接口。对于怎样实现你自己作用域的想法,请看Spring框架本身提供的Scope
实现和Scope
文档,它们解释了你需要实现的方法的更多细节。
Scope
接口有四个方法,从作用域中取得对象,从作用域中移除对象,并且允许它们被销毁。
下面的方法从潜在的作用域返回对象。session作用域实现,例如,返回具有session作用域的bean(如果它不存在,这个方法返回一个bean的新实例,然后绑定到session中准备将来引用)。
1 | Object get(String name, ObjectFactory objectFactory) |
下面的方法从潜在作用域中移除对象。以session作用域实现为例,从潜在的session中移除session作用域的bean。对象应该被返回,但如果没有找到指定名字的对象会返回空。
1 | Object remove(String name) |
下面的方法是注册当作用域销毁时或当作用域中的指定对象销毁时,作用域应该执行的回调函数。销毁回调函数的更多信息请看文档或Spring作用域实现。
1 | void registerDestructionCallback(String name, Runnable destructionCallback) |
下面的方法是获得潜在作用域的会话标识符。每个作用域的标识符都是不同的。对于session作用域实现,标识符是session标识符。
1 | String getConversationId() |
使用定制作用域
在你编写和测试一个或多个定制Scope
实现之后,你需要让Spring容器感知到你的新作用域。下面是在Spring容器中注册一个新Scope
的主要方法:
1 | void registerScope(String scopeName, Scope scope); |
这个方法是在ConfigurableBeanFactory
接口中声明的,在大多数具体的ApplicationContext
实现中都可获得,在Spring中通过BeanFactory
属性得到。
registerScope(..)
方法中的第一个参数是关于作用域的唯一名字;Spring容器本身中的这种名字的例子是singleton
和prototype
。registerScope(..)
方法中的第二个参数是你想注册和使用的定制Scope
实现的真正实例。
假设你编写了你的定制Scope
实现并按如下注册。
下面的例子使用Spring包含的
SimpleThreadScope
,但默认是不注册的。这个用法说明与你自己的定制Scope
是一样的。
1 | Scope threadScope = new SimpleThreadScope(); |
然后创建具有你自己定制的Scope
规则的bean定义:
1 | <bean id="..." class="..." scope="thread"> |
在定制Scope
实现后,你不会受限于作用域的程序注册。你也可以声明式的进行Scope
注册,使用CustomScopeConfigurer
类:
1 | <?xml version="1.0" encoding="UTF-8"?> |
当你在
FactoryBean
实现中放入<aop:scoped-proxy/>
时,它是工厂bean本身具有作用域,不是从getObject()
中返回的对象。
3.6 定制bean特性
3.6.1 生命周期回调
为了与容器中bean生命周期的管理进行交互,你可以实现Spring的InitializingBean
和DisposableBean
接口。当初始化beans时容器会调用InitializingBean
中的afterPropertiesSet()
方法,当销毁beans时容器会调用DisposableBean
中的destroy()
方法,在这两个方法中bean可以执行特定的行为。
在现代Spring应用中,通常认为JSR-250的
@PostConstruct
和@PreDestroy
注解是最佳实践接收生命周期回调函数的方法。使用这些注解意味着你的bean没有耦合Spring特定的接口。更多细节请看3.9.8小节,”@PostConstruct和@PreDestroy”。如果你不想使用JSR-250注解,但你仍要注意解耦,可以考虑使用对象定义元数据中的初始化方法和销毁方法。
在Spring内部,Spring框架使用BeanPostProcessor
实现来处理任何它能发现的回调接口并调用合适的方法。如果你需要定制Spring不能提供的开箱即用的功能或其它生命周期行为,你可以自己实现BeanPostProcessor
。更多信息请看3.8小节,”容器扩展点”。
除了初始化回调函数和销毁回调函数之外,Spring管理的对象也可以实现Lifecycle
接口,这些对象可以参与容器自身生命周期驱动的启动和关闭过程。
本节描述了生命周期回调接口。
初始化回调函数
org.springframework.beans.factory.InitializingBean
接口在容器设置了bean所有的必须属性之后,允许bean执行初始化工作。InitializingBean
接口指定了一个方法:
1 | void afterPropertiesSet() throws Exception; |
建议你不使用InitializingBean
接口,因为它对代码与Spring进行了不必要的耦合。作为一种替代方法,你可以使用@PostConstruct
注解或指定一个POPJO的初始化方法。在基于XML配置元数据的情况下,你可以使用init-method
特性来指定方法的名称,方法是没有返回值和参数的。如果使用Java配置,你可以使用@Bean
的initMethod
特性,请看”接收生命周期回调函数”小节。例如,下面的代码:
1 | <bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/> |
1 | public class ExampleBean { |
等价于:
1 | public class AnotherExampleBean implements InitializingBean { |
但没有与Spring代码耦合。
销毁回调函数
实现org.springframework.beans.factory.DisposableBean
接口允许容器包含的bean销毁时调用回调函数。DisposableBean
接口指定了一个方法:
1 | void destroy() throws Exception; |
建议你不使用DisposableBean
回调接口,因为它对代码与Spring进行了不必要的耦合。作为一种替代方法,你可以使用@PreDestroy
注解或指定一个bean定义支持的通用方法。在基于XML配置元数据的情况下,你可以使用<bean/>
的destroy-method
特性。如果使用Java配置,你可以使用@Bean
的destroyMethod
特性,请看”接收生命周期回调”小节。例如,下面的定义:
1 | <bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/> |
1 | public class ExampleBean { |
等价于:
1 | <bean id="exampleInitBean" class="examples.AnotherExampleBean"/> |
1 | public class AnotherExampleBean implements DisposableBean { |
但没有与Spring代码耦合。
默认初始化和销毁方法
当你编写初始化回调函数和析构回调函数时,不要使用Spring特定的InitializingBean
和DisposableBean
回调接口,自己编写方法,方法名通常为init()
,initialize()
,dispose()
等等。理想情况下,这种生命周期回调方法的名称在整个工程中是标准化的,以便所有开发人员使用同样的方法名称,保证一致性。
你可以配置Spring容器查找每个bean的初始化方法和析构方法时的名字。这意味着,作为一个应用开发者,你可以编写应用程序类并使用名为init()
的初始化回调方法,而不必在每个bean定义中配置init-method="init"
特性。当bean创建时,Spring Ioc容器调用这个方法(按照前面描述的标准生命周期回调约定)。这个功能也强制了初始化方法和析构方法命名规范的一致性。
假设你的初始化回调方法名为init()
,析构回调方法名为destroy()
。你的类应该与下面例子中的类类似。
1 | public class DefaultBlogService implements BlogService { |
1 | <beans default-init-method="init"> |
位于顶层<beans/>
元素中的default-init-method
特性,会让Spring IoC容器将beans中的名为init
的方法识别为初始化回调方法。当一个bean创建和组装时,如果bean类有这样一个方法,它会在恰当的时间被调用。
在bean被提供了所有依赖之后,Spring容器确保会立刻调用配置的初始化回调方法。因此初始化回调会在原生bean引用上调用,这意味着AOP拦截器等仍不能应用到bean中。首先要完整的创建目标bean,然后才会应用AOP代理(例如)等拦截器链。如果分别定义了目标bean和代理,你的代码甚至能绕过代理直接与原生的目标bean进行交互。将拦截器应用到初始化方法上可能会产生不一致性,因为这样做会使目标bean的生命周期与它的代理/拦截器相耦合,当你的代码与原生目标bean直接进行交互时,语义会变的很奇怪。
组合生命周期机制
从Spring 2.5开始,在控制bean的生命周期行为时,你有三中选择:InitializingBean和
DisposableBean回调接口;定制
init()和
destroy()方法;
@PostConstruct和
@PreDestroy`注解。在控制一个给定bean时你可以组合这些机制。
如果一个bean配置了多生命周期机制,每种机制配置了一个不同的方法名,那么每一个配置的方法会按照下面的顺序列表来执行。但是如果配置了相同的名字——例如,
init()
初始化方法——不止在一个生命周期机制中配置,那么这个方法只能执行一次,像之前所说的那样。
同一个bean配置了多生命周期机制,并有不同的初始化方法,那么调用顺序如下:
先调用有注解
@PostConstruct
的方法然后调用
InitializingBean
回调接口定义的afterPropertiesSet()
方法最好调用定制配置的
init()
方法
Destroy methods are called in the same order:
Methods annotated with
@PreDestroy
destroy()
as defined by theDisposableBean
callback interfaceA custom configured
destroy()
method
析构方法按同样的顺序调用:
先调用有
@PreDestroy
注解的方法再调用
DisposableBean
回调接口定义的destroy()
方法最好调用定制配置的
destroy()
方法
启动和关闭回调
Lifecycle
接口定义了任何对象生命周期都需要的基本方法(例如启动和停止一些背景处理):
1 | public interface Lifecycle { |
任何Spring管理的对象都可以实现那个接口。当ApplicationContext
本身收到启动启动和关闭信号时,例如运行时关闭/再启动场景,它将级联调用所有的上下文定义的Lifecycle
实现。它通过委托LifecycleProcessor
来完成这个功能:
1 | public interface LifecycleProcessor extends Lifecycle { |
注意LifecycleProcessor
本身是Lifecycle
接口的一个扩展。它也添加了两个其它的方法来响应上下文的再刷新和关闭的。
注意正规的
org.springframework.context.Lifecycle
接口只是一个显式启动/关闭通知的协议,并不意味着在上下文刷新时自动启动。考虑实现org.springframework.context.SmartLifecycle
接口来实现对指定bean自动启动的细粒度控制(包括启动时期)。请注意停止通知不能保证在销毁之前到来:在正式关闭时,所有的Lifecycle
beans在通常的析构回调传播之前首先会收到停止通知;但是,在上下文使用期间进行热刷新或尝试取消再刷新,只会调用析构方法。
启动和关闭的调用顺序是很重要的。如果任何两个对象间存在一个”depends-on”关系,那么依赖关系将在它的依赖之后开始,在它的依赖之前停止。然而有时直接的依赖关系是未知的。你可能只知道某个类型的对象应该在另一个类型的对象之前启动。在那种情况下,SmartLifecycle
接口定义了另一种选择,也就是说getPhase()
定义在它的父接口Phased
中。
1 | public interface Phased { |
1 | public interface SmartLifecycle extends Lifecycle, Phased { |
当开始时,最低相位的对象先启动,当停止时,最高相位的对象先停止。因此,实现了SmartLifecycle
接口,getPhase()
方法返回值为Integer.MIN_VALUE
的对象将最先启动并最后停止。另一方面,相位值Integer.MAX_VALUE
表明对象应该最后启动,最先停止(可能是因为它依赖其它运行的进程)。当考虑相位值时,知道任何没有实现SmartLifecycle
接口的Lifecycle
对象的默认值为0是很重要的。因此,任何负相位值表示对象应该在那么标准组件之前启动(在它们之后停止),反之为任何正相位值。
正如你看到的,在SmartLifecycle
中定义的停止方法接收一个回调函数。任何实现在关闭进程完成之后都必须调用回调的run()
方法。当需要时这可以进行异步关闭,因为LifecycleProcessor
接口、DefaultLifecycleProcessor
接口的默认实现会等待每个阶段的对象组直到达到超时值,然后调用回调函数。默认每个阶段的超时值为30秒。你可以在上下文中通过定义名为”lifecycleProcessor”的bean来覆盖默认的生命周期处理器实例。如果你只想修改超时值,如下定义是足够的:
1 | <bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor"> |
像上面提到的那样,LifecycleProcessor
接口为再刷新和上下文的关闭也定义了回调方法。后者会简单的驱动关闭进程就像显式的调用了stop()
方法一样,但当上下文关闭时它才会发生。另一方面refresh
回调能使SmartLifecycle
beans的另一个功能可用。当上下文再刷新时(所有对象已经实例化并初始化),回调函数将被调用,那时默认的生命周期处理器将会检查每个SmartLifecycle
对象的isAutoStartup()
方法返回的布尔值。如果为true
,对象将会在那时启动而不是等待上下文的显式调用或它自己的start()
方法(不像上下文再刷新,对于一个标准的上下文实现上下启动不会自动发生)。”phase”值以及”depends-on”关系将决定启动顺序,像上面描述的一样。
在非web应用中妥善的关闭Spring IoC容器
这一节只应用于非web应用。Spring的基于web的
ApplicationContext
实现已经有代码来处理当相关的web应用关闭时,妥善关闭Spring IoC容器的问题。
如果你在非web应用环境使用Spring的IoC容器;例如,在一个富桌面客户端环境中,你在JVM中注册一个关闭钩子。这样做确保了妥善的关闭,为了释放所有资源需要调用与单例beans相关的析构方法。当然,你仍然必须正确的配置和实现这些销毁回调函数。
为了注册一个关闭钩子,你可以调用ConfigurableApplicationContext
接口中声明的registerShutdownHook()
方法:
1 | import org.springframework.context.ConfigurableApplicationContext; |
3.6.2 ApplicationContextAware和BeanNameAware
当ApplicationContext
创建一个实现org.springframework.context.ApplicationContextAware
接口的对象实例时,这个实例会提供一个ApplicationContext
的引用。
1 | public interface ApplicationContextAware { |
因此beans可以以编程方式操纵创建它们的ApplicationContext
,通过ApplicationContext
接口,或通过将引用抛给这个接口的一个已知子类,例如ConfigurableApplicationContext
,它暴露了额外的功能。一个方法是编程式检索其他的bean。有时这个能力是很有用的,但是通常你应该避免使用它,因为它耦合了代码和Spring,不能遵循控制反转的风格,在控制反转中协作者是作为属性提供给beans的。ApplicationContext
的其它方法提供了对文件资源的访问,发布应用事件,访问MessageSource
的功能。这些额外的特性将在3.15小节『ApplicationContext”的额外能力』中描述。
从Spring 2.5起,自动装配是另一种可替代的获得ApplicationContext
引用的方法。『传统的』constructor
和byType
自动装配模式(如3.4.5小节所述,『自动装配协作者』)可以分别为构造函数参数或setter方法参数提供ApplicationContext
类型的依赖。更多的灵活性包括自动装配变量的能力和多参数方法,使用新的基于注解的自动装配特性。如果你这一做的话,ApplicationContext
可以被自动装配到变量中,构造函数参数中或方法参数中,如果讨论的变量,构造函数或方法有@Autowired
注解,那么可以期望它是ApplicationContext
类型。更多信息请看3.9.2小节,@autowired
。
当ApplicationContext
创建一个实现了org.springframework.beans.factory.BeanNameAware
接口的类时,类中有相关的对象定义中定义的名称的引用。
1 | public interface BeanNameAware { |
在正常的bean属性填入之后,回调方法调用,但在初始化回调方法之前,例如InitializingBean
的afterPropertiesSet或一个定制的初始化方法。
3.6.3 其它的Aware接口
除了上面讨论的ApplicationContextAware
和BeanNameAware
之外,Spring给予了一系列Aware
接口来允许beans向容器表明它们需要一个确定的基础结构依赖。最重要的Aware
接口总结如下——作为一个通用规则,名字是依赖类型的一个很好暗示:
表3.4. Aware接口
Name | Injected Dependency | Explained in |
---|---|---|
ApplicationContextAware | 声明ApplicationContext |
Section 3.6.2, “ApplicationContextAware and BeanNameAware” |
ApplicationEventPublisherAware | 封装事件发布的ApplicationContext |
Section 3.15, “Additional Capabilities of the ApplicationContext” |
BeanClassLoaderAware | 用来加载bean的类加载器 | Section 3.3.2, “Instantiating beans” |
BeanFactoryAware | 声明BeanFactory |
Section 3.6.2, “ApplicationContextAware and BeanNameAware” |
BeanNameAware | 声明的bean的名字 | Section 3.6.2, “ApplicationContextAware and BeanNameAware” |
BootstrapContextAware | 容器运行的资源自适应BootstrapContext . 通常只在JCA aware ApplicationContexts 可获得 |
Chapter 28, JCA CCI |
LoadTimeWeaverAware | 加载时为处理类定义定义的weaver | Section 7.8.4, “Load-time weaving with AspectJ in the Spring Framework” |
MessageSourceAware | 解析消息配置策略 (支持参数化和国际化) | Section 3.15, “Additional Capabilities of the ApplicationContext” |
NotificationPublisherAware | Spring JMX通知发布器 | Section 27.7, “Notifications” |
ResourceLoaderAware | 为底层访问资源配置的加载器 | Chapter 4, Resources |
ServletConfigAware | 容器运行的当前ServletConfig 。 仅在web感知的Spring ApplicationContext 中有效 |
Chapter 18, Web MVC framework |
ServletContextAware | 容器运行的当前ServletContext 。 仅在web感知的Spring ApplicationContext 中有效 |
Chapter 18, Web MVC framework |
注意这些接口的用法将你的代码与Spring进行了捆绑,不符合控制反转的风格。因此,它们是为那么需要以编程方式访问容器的基础结构beans推荐的。
3.7 Bean定义继承
Bean定义中可以包含许多配置信息,包括构造函数参数,属性值和容器的特定信息例如初始化方法,静态工厂方法名等等。子定义继承父定义的配置信息。子定义可以覆盖一些值,或按需要添加一些其它值。使用父子bean定义可以保存许多类型。实际上,这是一种模板形式。
如果你以编程方式使用ApplicationContext
接口,那么子bean定义是通过ChildBeanDefinition
定义来表示的。大多数用户不在这个层级上使用它们,而是在一些像ClassPathXmlApplicationContext
中声明式的配置bean定义。当你使用基于XML的配置元数据时,你可以使用parent
特性来指明一个子bean定义。在特性值中指定父bean。
1 | <bean id="inheritedTestBean" abstract="true" |
如果子bean定义中没有指定要使用的bean类,则使用父定义中的bean类,但也可以覆盖它。在后一种情况下(没有指定要用的bean类),子bean定义必须与父bean协作,也就是说,它必须接收父定义的属性值。
子bean定义可以继承作用域,构造函数参数值,属性值,可以重写父方法,可以选择添加新值。你指定的任何作用域,初始化方法,析构方法,和/或静态工厂方法设置将会覆盖对应的父设置。
其余的设置都是从子定义中获取:依赖关系,自动装配模式,依赖检查,单例,延迟初始化。
前面的例子使用抽象特性将父bean定义显式的标记为abstract
。如果父定义没有指定一个类,需要显式的将父bean定义为abstract
,形式如下:
1 | <bean id="inheritedTestBeanWithoutClass" abstract="true"> |
父bean不能实例化,因为它不完整,并且它被显式的标记为abstract
。当一个bean定义是abstract
时,它只能是一个纯粹的bean定义模板,作为一个为子定义服务的父定义。当试图使用一个abstract
父bean时,可以通过另一个bean的ref
属性来引用它或通过父bean的id为参数显式的调用getBean()
方法,会返回一个错误。类似的,容器内部的preInstantiateSingletons()
方法会忽略抽象bean定义。
默认情况下
ApplicationContext
会预实例化所有的单例。因此,如果你想有一个(父)bean定义只作为模板来使用,这个定义中指定了一个类,那你必须确保设置abstract
特性为true
,否则应用上下文会(试图)预实例化这个abstract
bean。
3.8 容器扩展点
通常情况下,应用开发者不需要继承ApplicationContext
的实现类。反而是Spring的IoC容器可以通过插入特定集成接口的实现来进行扩展。下面几节将描述这些集成接口。
3.8.1 通过BeanPostProcessor定制bean
BeanPostProcessor
接口定义了回调方法,你可以实现这个方法来提供你自己的(或覆盖容器默认的)实例化逻辑,依赖解析逻辑等等。如果你想在Spring容器完成实例化,配置和初始化bean之后实现一些定制的业务逻辑,你可以插入一个或多个BeanPostProcessor
实现。
你可以配置多个BeanPostProcessor
实例,通过设置order
属性你可以控制BeanPostProcessors
的执行顺序。只有BeanPostProcessor
实现了Ordered
接口时你才可以设置这个属性;如果你编写了你自己的BeanPostProcessor
,你也应该考虑实现Ordered
接口。更多细节请参考BeanPostProcessor
接口和Ordered
接口的Java文档。也可以查看下面的BeanPostProcessors
编程注册的笔记。
BeanPostProcessors
操作一个bean(或对象)实例;也就是说,Spring Ioc容器实例化一个bean实例,然后BeanPostProcessors
完成它们的工作。
BeanPostProcessors
的作用域是每个容器。只有你在使用容器分层的情况下,这才是相关的。如果你在一个容器中定义了一个BeanPostProcessor
,它将只后处理容器中的beans。换句话说,某个容器中定义的beans不能被另一个容器中定义的BeanPostProcessor
进行后处理,即使这两个容器是同一层上的一部分。为了改变实际的bean定义(例如,定义bean的蓝图),你可以使用3.8.2小节中描述的
BeanFactoryPostProcessor
。
org.springframework.beans.factory.config.BeanPostProcessor
接口包含恰好两个回调方法。当这样一个类在容器中注册为后处理器时,对于容器中创建的每一个bean实例,在容器初始化方法(例如InitializingBean
的afterPropertiesSet()
方法和任何已声明的初始化方法)被调用之前和任何bean初始化回调函数之后,后处理器会从容器中得到一个回调函数。后处理器可以对bean实例进行任何操作,包括完全忽略回调方法。bean后处理器通常检查回调接口或将bean包裹到代理中。为了提供代理包裹逻辑,一些Spring AOP基础结构类被实现为bean后处理器。
ApplicationContext
会自动检测任何配置元数据中定义的实现了BeanPostProcessor
接口的bean。为了能在后面bean创建时调用这些bean,ApplicationContext
会将这些bean注册为后处理器。bean后处理器可以像其它bean一样在容器进行部署。
注意当在一个配置类上使用@Bean
声明一个BeanPostProcessor
时,工厂方法的返回值应该是实现类本身或是org.springframework.beans.factory.config.BeanPostProcessor
接口,这能清晰的表明bean的后处理器特性。此外,在完整的创建它之前,ApplicationContext
不能通过类型自动检测它。由于BeanPostProcessor
需要早一点实例化,为了在上下文中初始化其它的beans,早期的类型检测是非常关键的。
实现
BeanPostProcessor
接口的类是特别的并被容器不同对待。所有的BeanPostProcessors
和它们直接引用的beans在启动时进行实例化,它们是ApplicationContext
特定启动阶段的一部分。接下来,所有BeanPostProcessors
以有序形式进行注册,并适用于容器中所有更进一步的beans。由于AOP自动代理是作为BeanPostProcessor
本身实现的,既不是BeanPostProcessors
也不是它们直接引用的beans适合进行自动代理,因此没有融入它们的方面。对于这样的bean,你应该看到一个信息日志消息:”Bean foo没资格被所有的
BeanPostProcessor
接口进行处理(例如,不适合自动代理)。”。注意如果有beans使用自动装配或
@Resource
(可能回到自动装配)注入你的BeanPostProcessor
,当搜索类型匹配的依赖候选者时,Spring可能访问未预料到beans,因此使它们不适合自动代理或其他类型的进行后处理的bean。例如,如果你有一个带有@Resource
注解的依赖,field/setter
名称不能直接对应bean声明的名字,也没有使用name特性,Spring将通过类型匹配来访问其它的bean。
下面的例子展示了在ApplicationContext
中如何编写,注册和使用BeanPostProcessors
。
例子: Hello World, BeanPostProcessor类型
第一个例子阐述了基本用法。这个例子展示了一个定制BeanPostProcessor
实现,实现中调用了每一个bean的toString()
方法。当容器创建它时,会将结果字符串输出到系统控制台。
下面是定制BeanPostProcessor
实现的类定义:
1 | package scripting; |
1 | <?xml version="1.0" encoding="UTF-8"?> |
注意InstantiationTracingBeanPostProcessor
是怎样简单定义的。它甚至没有一个名字,因为它是一个bean,它能像其它bean一样进行依赖注入。(前面的配置也定义了一个bean,它被Groovy脚本支持。Spring动态语言支持在31章『动态语言支持』中进行了详细描述。)
下面的简单Java应用执行了前面的代码和配置:
1 | import org.springframework.context.ApplicationContext; |
前面的应用输出结果如下:
1 | Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961 |
例: RequiredAnnotationBeanPostProcessor
使用回调函数接口或注解结合定制BeanPostProcessor
实现是扩展Spring IoC容器的常见方法。一个例子是Spring的RequiredAnnotationBeanPostProcessor
——一个BeanPostProcessor
实现附带在Spring发行中,它保证了标记有(任意)注解的beans上的JavaBean
属性能真正(配置成)通过值进行依赖注入。
3.8.2 通过BeanFactoryPostProcessor定制配置元数据
接下来我们要看到的扩展点是org.springframework.beans.factory.config.BeanFactoryPostProcessor
。这个接口的语义与那些BeanPostProcessor
类似,但有一个主要的不同:BeanFactoryPostProcessor
可以操作配置元数据;也就是说,Spring IoC容器允许在容器实例化除了BeanFactoryPostProcessor
之外的任何beans之前,BeanFactoryPostProcessor
读取配置元数据并可能修改它们。
你可以配置多个BeanFactoryPostProcessors
,你可以通过设置order
属性来控制这些BeanFactoryPostProcessors
的执行顺序。但是,只有BeanFactoryPostProcessor
实现了Ordered
接口时你才可以设置这个属性。如果你编写了你自己的BeanFactoryPostProcessor
,你也应该考虑实现Ordered
接口。关于BeanFactoryPostProcessor
和Ordered
的更多细节请看文档。
如果你想改变真正的bean实例(例如,从配置元数据中创建的对象),你应该需要使用
BeanPostProcessor
(3.8.1小节中描述的)。尽管在BeanFactoryPostProcessor
中处理bean实例在技术上是可能的(例如使用BeanFactory.getBean()
),但这样做会引起过早的bean实例化,违背标准的容器生命周期。这可能会产生负面影响例如绕过bean后处理。
BeanFactoryPostProcessors
的作用域也是在每个容器中。这仅对于容器分层而言。如果在一个容器中你定义了一个BeanFactoryPostProcessor
,它将适用于那个容器中的bean定义。一个容器中的bean定义不能被另一个容器中的BeanFactoryPostProcessors
进行后处理,即使两个容器是在同一个分层中。
为了修改定义在容器中的配置元数据,当一个bean工厂后处理器在ApplicationContext
中声明时,它会自动执行。Spring包含许多预先定义的bean工厂后处理器,例如PropertyOverrideConfigurer
和PropertyPlaceholderConfigurer
。定制的BeanFactoryPostProcessor
也可以使用,例如,为了注册定制的属性编辑器。
ApplicationContext
会自动检测任何部署在它之内的实现了BeanFactoryPostProcessor
接口的bean。在合适的时间,它会使用这些beans作为bean工厂后处理器。你可以像任何你使用的bean那样部署这些后处理器beans。
关于
BeanPostProcessors
, 通常情况下你不想配置BeanFactoryPostProcessors
为延迟初始化。 如果没有别的bean引用Bean(Factory)PostProcessor
,后处理器将不会实例化。因此,对它进行延迟初始化会被忽略,即使你将<beans/>
元素中的default-lazy-init
特性设置为true
,Bean(Factory)PostProcessor
也会急切的初始化。
例: 类名替换PropertyPlaceholderConfigurer
你可以使用PropertyPlaceholderConfigurer
读取单独文件中的bean定义来使属性具体化,这个单独文件使用标准的Java Properties
格式。这样做可以在部署应用时定制特定环境属性例如数据库URLs和密码,没有复杂性或修改主XML定义文件及容器相关文件的风险。
考虑一下下面的基于XML定义的配置元数据片段,其中定义了一个带有占位符的DataSource
。这个例子展示了从外部Properties
文件进行属性配置。在运行时,PropertyPlaceholderConfigurer
会应用到元数据中,将会替换DataSource
中的一些属性。通过${property-name}
形式的占位符指定要替换的值,这遵循了Ant/log4j/JSP EL风格。
1 | <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> |
真正的属性值来自于另一个以标准Java Properties
形式编写的文件:
1 | jdbc.driverClassName=org.hsqldb.jdbcDriver |
因此,在运行是字符串${jdbc.username}
被替换为sa
,其它的匹配属性文件中的key的占位符的值以同样方式替换。PropertyPlaceholderConfigurer
会检查bean中大多数属性和特性的占位符。此外,占位符的前缀和后缀都可以定制。
Spring 2.5中引入了上下文命名空间,可以通过专用配置元素配置属性占位符。在location
特性可以提供一个或多个位置,多个位置用逗号分开。
1 | <context:property-placeholder location="classpath:com/foo/jdbc.properties"/> |
PropertyPlaceholderConfigurer
不仅仅查找指定Properties
文件中的属性。默认情况下,如果不能在指定属性文件中找到属性,它也检查Java System
属性。你可以通过下面三个支持的整数值中的一个设置配置器的systemPropertiesMode
属性,从而定制查找行为。
never (0): 从不检查
system
属性fallback (1): 如果不能在指定文件中解析属性,检查
system
属性,这是默认值。override (2): 在查找指定文件之前,首先检查
system
属性,这可以使系统属性覆盖任何其它属性源。
更多信息请看PropertyPlaceholderConfigurer
文档。
你可以
PropertyPlaceholderConfigurer
替换类名,有时候非常有用,特别是运行时你必须选择一个特别的实现类的情况下。例如:
1 | <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> |
如果这个类不能在运行时解析成一个有效类,对于一个非懒惰初始化的bean,当它要创建时,在
ApplicationContext
的preInstantiateSingletons()
期间,bean会解析失败。
例: PropertyOverrideConfigurer
PropertyOverrideConfigurer
,另一个bean工厂后处理器,类似于PropertyPlaceholderConfigurer
,但不像后者,最初的定义可以有默认值或bean属性一点也没有值。如果一个覆写的Properties
文件对于某个bean属性没有任何输入,会使用默认的上下文定义。
注意bean定义没有意识到被覆写了,因此从XML定义文件中它不能立刻很明显的看出在使用覆写的配置器。为了防止多个PropertyOverrideConfigurer
实例对于同一个bean属性定义不同的值,根据覆写机制,使用最后一个定义的值。
属性文件配置形式如下:
1 | beanName.property=value |
例如:
1 | dataSource.driverClassName=com.mysql.jdbc.Driver |
例子文件可以被包含名为dataSource
bean的容器定义使用,它有一个driver
和url
属性。
混合属性命名也支持,除了最后被覆写的属性,只要路径的每部分都已经是非空(假设构造函数进行初始化)。在这个例子中:
1 | foo.fred.bob.sammy=123 |
foo
bean中的fred
属性的bob
属性的sammy
属性设为标量值123
。
指定的覆写值总是字面值;它们不能转成bean引用。当XML bean定义中的初始值指定了一个bean引用时,这个规范同样有效。
Spring 2.5引入了上下文命名空间,可以用专用配置元素配置属性覆写:
1 | <context:property-override location="classpath:override.properties"/> |
3.8.3 使用FactoryBean定制实例化逻辑
为对象实现org.springframework.beans.factory.FactoryBean
接口的是工厂本身。
FactoryBean
接口是Spring IoC的实例化逻辑可插入性的一个点。如果你有复杂的初始化代码,相比于大量的冗余的XML代码用Java语言来表达会更好,那么你可以创建你自己的FactoryBean
,在类里面编写复杂的初始化逻辑,并将你定制的FactoryBean
插入到容器中。
FactoryBean
接口提供了三个方法:
Object getObject()
: 返回一个工厂创建的对象实例。这个实例可能被共享, 依赖于工厂是否返回一个单例或原型。boolean isSingleton()
: 如果FactoryBean
返回单例,返回true,否则返回false。Class getObjectType()
: 返回getObject()
方法返回的类型,如果类型不能提前知道则返回null。
FactoryBean
的概念和接口在Spring框架中的许多地方都使用了;Spring本身中有不止50个FactoryBean
接口的实现。
当你需要向容器请求一个真正的FactoryBean
实例本身来代替它产生的bean时,调用ApplicationContext
的getBean()
方法时,bean的id前面要加上一个$
符。因此给定一个id为myBean
的FactoryBean
,在容器中调用getBean("myBean")
,返回FactoryBean
的产品,但调用getBean("&myBean")
会返回FactoryBean
实例本身。
3.9 基于注解的容器配置
在配置Spring时注解是否比XML更好?
基于注解配置的引入引出了一个问题——这种方式是否比基于XML的配置更好。简短的回答是视情况而定。长一点的回答是每种方法都有它的优点和缺点,通常是由开发者决定哪一种策略更适合他们。由于注解的定义方式,注解在它们的声明中提供了许多上下文,导致配置更简短更简洁。然而,XML擅长连接组件而不必接触源代码或重新编译它们。一些开发者更喜欢接近源代码,而另一些人则认为基于注解的类不再是POJOs,此外,配置变的去中心化,而且更难控制。
无论选择是什么,Spring都能容纳这两种风格,甚至可以将它们混合在一起。值得指出的是,通过它的Java配置选项,Spring允许注解以一种非入侵的方式使用,不触碰目标组件源码和那些工具,所有的配置风格由Spring工具套件支持。
基于注解的配置提供了一种XML设置的可替代方式,它依赖于字节码元数据来连接组件,而不是用尖括号声明的方式。代替使用XML来描述bean连接,开发者通过将注解使用在相关的类,方法或字段声明中,将配置移动到了组件类本身的内部。正如在“Example: The RequiredAnnotationBeanPostProcessor”那节提到的那样,使用BeanPostProcessor
与注解结合是扩展Spring IoC容器的的常见方法。例如,Spring 2.0引入了@Required
注解来执行需要的属性的可能性。Spring 2.5使以同样地通用方法来驱动Spring的依赖注入变为可能。本质上来说,@Autowired
提供了如3.4.5小节描述的同样的能力。“Autowiring collaborators”但更细粒度的控制和更广的应用性。Spring 2.5也添加对JSR-250注解的支持,例如,@PostConstruct
和@PreDestroy
。Spring 3.0添加了对JSR-330,包含在javax.inject
包内的注解(Java的依赖注入)的支持,例如@Inject
和@Named
。关于这些注解的细节可以在相关的小节找到。
注解注入在XML注入之前进行,因此对于通过两种方法进行组装的属性后者的配置会覆盖前者。
跟以前一样,你可以作为单独的bean定义来注册它们,但也可以通过在一个基于XML的Spring配置(注入包含上下文命名空间)中包含下面的标签来隐式的注册它们:
1 | <?xml version="1.0" encoding="UTF-8"?> |
(隐式注册的后处理器包括 AutowiredAnnotationBeanPostProcessor
,CommonAnnotationBeanPostProcessor
,PersistenceAnnotationBeanPostProcessor
和前面提到的RequiredAnnotationBeanPostProcessor
。)
<context:annotation-config/>
仅在定义它的同样的应用上下文中寻找注解的beans。这意味着,如果你在一个为DispatcherServlet
服务的WebApplicationContext
中放置了<context:annotation-config/>
,它只能在你的控制器中寻找@Autowired
注解的beans,而不是在你的服务层中。更多信息请看18.2小节,“The DispatcherServlet”。
3.9.1 @Required
@Required
注解应用到bean属性的setter方法上,例子如下:
1 | public class SimpleMovieLister { |
这个注解仅仅是表明受影响的bean属性必须在配置时通过显式的bean定义或自动组装填充。如果受影响的bean属性没有填充,容器会抛出一个异常,这允许及早明确的失败,避免NullPointerExceptions
或后面出现类似的情况。仍然建议你在bean类本身加入断言,例如,加入到初始化方法中。这样做可以强制这些需要的引用和值,甚至是你在容器外部使用这个类的时候。
3.9.2 @Autowired
在下面的例子中JSR 330的
@Inject
注解可以用来代替Spring的@Autowired
注解。
你可以将@Autowired
注解应用到构造函数上。
1 | public class MovieRecommender { |
从Spring框架4.3起,如果目标bena仅定义了一个构造函数,那么
@Autowired
注解的构造函数不再是必要的。如果一些构造函数是可获得的,至少有一个必须要加上注解,以便于告诉容器使用哪一个。
正如预料的那样,你也可以将@Autowired
注解应用到“传统的”setter方法上:
1 | public class SimpleMovieLister { |
你也可以应用注解到具有任何名字和/或多个参数的方法上:
1 | public class MovieRecommender { |
你也可以应用@Autowired
到字段上,甚至可以与构造函数混合用:
1 | public class MovieRecommender { |
通过给带有数组的字段或方法添加@Autowired
注解,也可以从ApplicationContext
中提供一组特定类型的bean:
1 | public class MovieRecommender { |
同样也可以应用到具有同一类型的集合上:
1 | public class MovieRecommender { |
如果你希望数组或列表中的项按指定顺序排序,你的bean可以实现
org.springframework.core.Ordered
接口,或使用@Order
或标准@Priority
注解。
只要期望的key是String
,那么类型化的Maps就可以自动组装。Map的值将包含所有期望类型的beans,key将包含对应的bean名字:
1 | public class MovieRecommender { |
默认情况下,当没有候选beans可获得时,自动组装会失败;默认的行为是将注解的方法,构造函数和字段看作指明了需要的依赖。这个行为也可以通过下面的方式去改变。
1 | public class SimpleMovieLister { |
每个类只有一个构造函数可以标记为必需的,但可以注解多个非必需的构造函数。在这种情况下,会考虑这些候选者中的每一个,Spring使用最贪婪的构造函数,即依赖最满足的构造函数,具有最大数目的参数。
建议在
@Required
注解之上使用@Autowired
的required
特性。required
特性表明这个属性自动装配是不需要的,如果这个属性不能被自动装配,它会被忽略。另一方面@Required
是更强大的,在它强制这个属性被任何容器支持的bean设置。如果没有值注入,会抛出对应的异常。
你也可以对那些已知的具有可解析依赖的接口使用@Autowired
:BeanFactory
,ApplicationContext
,Environment
, ResourceLoader
,ApplicationEventPublisher
和MessageSource
。这些接口和它们的扩展接口,例如ConfigurableApplicationContext
或ResourcePatternResolver
,可以自动解析,不需要特别的设置。
1 | public class MovieRecommender { |
@Autowired
,@Inject
,@Resource
和@Value
注解是通过SpringBeanPostProcessor
实现处理,这反过来意味着你不能在你自己的BeanPostProcessor
或BeanFactoryPostProcessor
中应用这些注解(如果有的话)。这些类型必须显式的通过XML或使用Spring的@Bean
方法来’wired up’。
3.9.3 用@Primary微调基于注解的自动装配
因为根据类型的自动装配可能会导致多个候选目标,所以在选择过程中进行更多的控制经常是有必要的。一种方式通过Spring的@Primary
注解来完成。当有个多个候选bean要组装到一个单值的依赖时,@Primary
表明指定的bean应该具有更高的优先级。如果确定一个’primary’ bean位于候选目标中间,它将是那个自动装配的值。
假设我们具有如下配置,将firstMovieCatalog
定义为主要的MovieCatalog
。
1 |
|
根据这样的配置,下面的MovieRecommender
将用firstMovieCatalog
进行自动装配。
1 | public class MovieRecommender { |
对应的bean定义如下:
1 | <?xml version="1.0" encoding="UTF-8"?> |
3.9.4 微调基于注解且带有限定符的自动装配
当有多个实例需要确定一个主要的候选对象时,@Primary
是一种按类型自动装配的有效方式。当需要在选择过程中进行更多的控制时,可以使用Spring的@Qualifier
注解。为了给每个选择一个特定的bean,你可以将限定符的值与特定的参数联系在一起,减少类型匹配集合。在最简单的情况下,这是一个纯描述性值:
1 | public class MovieRecommender { |
@Qualifier
注解也可以指定单个构造函数参数或方法参数:
1 | public class MovieRecommender { |
对应的bean定义如下。限定符值为”main”的bean被组装到有相同值的构造函数参数中。
1 | <?xml version="1.0" encoding="UTF-8"?> |
对于回退匹配,bean名字被认为是默认的限定符值。因此你可以定义一个id为main
的bean来代替内嵌的限定符元素,会有同样的匹配结果。然而,尽管你可以使用这个约定根据名字引用特定的beans,但是@Autowired
从根本上来讲是使用可选的语义限定符来进行类型驱动注入的。这意味着限定符的值,即使回退到bean名称,总是缩小语义类型匹配的集合;它们没有从语义上将一个引用表达为一个唯一的bean id。好的限定符值是”main”或”EMEA”或”persistent”,表达一个特定组件的性质,这个组件是独立于bean id
的,即使前面例子中像这个bean一样的匿名bean会自动生成id。
正如前面讨论的那样,限定符也可以应用到类型结合上,例如,Set<MovieCatalog>
。在这个例子中,根据声明的限定符匹配的所有beans作为一个集合进行注入。这意味着限定符不必是唯一的;它们只是构成过滤标准。例如,你可以定义多个具有同样限定符值”action”的MovieCatalog
,所有的这些都将注入到带有注解@Qualifier("action")
的Set<MovieCatalog>
中。
如果你想通过名字表达注解驱动的注入,不要主要使用
@Autowired
,虽然在技术上能通过@Qualifier
值引用一个bean名字。作为可替代产品,可以使用JSR-250@Resource
注解,它在语义上被定义为通过组件唯一的名字来识别特定的目标组件,声明的类型与匹配过程无关。@Autowired
有不同的语义:通过类型选择候选beans,特定的String
限定符值被认为只在类型选择的候选目标中,例如,在那些标记为具有相同限定符标签的beans中匹配一个”account”限定符。对于那些本身定义在集合/映射或数组类型中的beans来说,
@Resource
是一个很好的解决方案,适用于特定的集合或通过唯一名字区分的数组bean。也就是说,自Spring 4.3起,集合/映射和数组类型中也可以通过Spring的@Autowired
类型匹配算法进行匹配,只要元素类型信息在@Bean
中保留,返回类型签名或集合继承体系。在这种情况下,限定符值可以用来在相同类型的集合中选择,正如在前一段中概括的那样。自Spring 4.3起,
@Autowired
也考虑自引用注入,例如,引用返回当前注入的bean。注意自注入是备用;普通对其它组件的依赖关系总是优先的。在这个意义上,自引用不参与普通的候选目标选择,因此尤其是从不是主要的;恰恰相反,它们最终总是最低的优先级。在实践中,自引用只是作为最后的手段,例如,通过bean的事务代理调用同一实例的其它方法:在考虑抽出受影响的方法来分隔代理bean的场景中。或者,使用@Resource
通过它的唯一名字可能得到一个返回当前bean的代理。
@Autowired
可以应用到字段,构造函数和多参数方法上,允许通过限定符注解在参数层面上缩减候选目标。相比之下,@Resource
仅支持字段和bean属性的带有单个参数的setter方法。因此,如果你的注入目标是一个构造函数或一个多参数的方法,坚持使用限定符。
你可以创建自己的定制限定符注解。简单定义一个注解,在你自己的定义中提供@Qualifier
注解:
1 | ({ElementType.FIELD, ElementType.PARAMETER}) |
然后你可以在自动装配的字段和参数上提供定制的限定符:
1 | public class MovieRecommender { |
接下来,提供候选bean定义的信息。你可以添加<qualifier/>
标记作为<bean/>
标记的子元素,然后指定匹配你的定制限定符注解的类型和值。类型用来匹配注解的全限定类名称。或者,如果没有名称冲突的风险,为了方便,你可以使用简写的类名称。下面的例子证实了这些方法。
1 | <?xml version="1.0" encoding="UTF-8"?> |
在3.10小节,“类路径扫描和管理组件”中,你将看到一个基于注解的替代方法,在XML中提供限定符元数据。特别地,看3.10.8小节,“用注解提供限定符元数据”。
在某些情况下,使用没有值的注解就是足够的。当注解为了通用的目的时,这是非常有用的,可以应用到跨几个不同类型的依赖上。例如,当网络不可用时,你可以提供一个要搜索的离线目录。首先定义一个简单的注解:
1 | ({ElementType.FIELD, ElementType.PARAMETER}) |
然后将注解添加到要自动装配的字段或属性上:
1 | public class MovieRecommender { |
现在bean定义只需要一个限定符类型:
1 | <bean class="example.SimpleMovieCatalog"> |
你也可以定义接收命名属性之外的定制限定符注解或代替简单的值属性。如果要注入的字段或参数指定了多个属性值,bean定义必须匹配所有的属性值才会被认为是一个可自动装配的候选目标。作为一个例子,考虑下面的注解定义:
1 | @Target({ElementType.FIELD, ElementType.PARAMETER}) |
这种情况下Format
是枚举类型:
1 | public enum Format { |
要自动装配的字段使用定制限定符进行注解,并且包含了两个属性值:genre
和format
。
1 | public class MovieRecommender { |
最后,bean定义应该包含匹配的限定符值。这个例子也证实了bean元属性可以用来代替<qualifier/>
子元素。如果可获得<qualifier/>
,它和它的属性优先级更高,如果当前没有限定符,自动装配机制会将<meta/>
内的值作为备用,正如下面的例子中的最后两个bean定义。
1 | <?xml version="1.0" encoding="UTF-8"?> |
3.9.5 使用泛型作为自动装配限定符
除了@Qualifier
注解外,也可以使用Java的泛型类型作为限定符的一种暗示方式。例如,假设你有如下配置:
1 | @Configuration |
假设上面的beans实现了一个泛型接口,例如,Store<String>
和Store<Integer>
,你可以@Autowire
Store
接口,泛型将作为限定符使用:
1 |
|
当自动装配Lists
,Maps
和Arrays
时,也会应用泛型限定符:
1 | // Inject all Store beans as long as they have an <Integer> generic |
3.9.6 CustomAutowireConfigurer
CustomAutowireConfigurer
是一个能使你注册自己的定制限定符注解类型的BeanFactoryPostProcessor
,即使它们不使用Spring的@Qualifier
注解进行注解。
1 | <bean id="customAutowireConfigurer" |
AutowireCandidateResolver
通过下面的方式决定自动装配的候选目标:
每个bean定义的
autowire-candidate
在
<beans/>
元素可获得的任何default-autowire-candidates
模式存在
@Qualifier
注解和任何在CustomAutowireConfigurer
中注册的定制注解
当多个beans符合条件成为自动装配的候选目标时,”primary” bean的决定如下:如果在候选目标中某个确定的bean中的primary
特性被设为true
,它将被选为目标bean。
3.9.7 @Resource
Spring也支持使用JSR-250 @Resource
对字段或bean属性setter方法进行注入。这是在Java EE 5和6中的一种通用模式,例如在JSF 1.2管理的beans或JAX-WS 2.0的端点。Spring对它管理的对象也支持这种模式。
@Resource
采用名字属性,默认情况下Spring将名字值作为要注入的bean的名字。换句话说,它遵循by-name语义,下面的例子证实了这一点:
1 | public class SimpleMovieLister { |
如果没有显式的指定名字,默认名字从字段名或setter方法中取得。在字段情况下,它采用字段名称;在setter方法情况下,它采用bean的属性名。因此下面的例子将名字为movieFinder
的bean注入到它的setter方法中:
1 | public class SimpleMovieLister { |
注解提供的名字被
CommonAnnotationBeanPostProcessor
感知的ApplicationContext
解析为bean名字。如果你显式地配置了Spring的SimpleJndiBeanFactory
,名字会通过JNDI解析。但是建议你依赖默认行为,简单使用Spring的JNDI查找功能保护间接查找级别。
在@Resource
特有的没有显式名字指定的情况下,类似于@Autowired
,@Resource
会进行主要的匹配类型来代替指定名字的bean并解析已知的可解析依赖:BeanFactory
,ApplicationContext
,ResourceLoader
,ApplicationEventPublisher
和MessageSource
接口。
因此在下面的例子中,customerPreferenceDao
字段首先查找名字为customerPreferenceDao
的bean,然后回退到主要的类型为CustomerPreferenceDao
的类型匹配。”context”字段会注入基于已知的可解析依赖类型ApplicationContext
。
1 | public class MovieRecommender { |
3.9.8 @PostConstruct和@PreDestroy
CommonAnnotationBeanPostProcessor
不仅识别@Resource
注解,而且识别JSR-250生命周期注解。在Spring 2.5引入了对这些注解的支持,也提供了在初始化回调函数和销毁回调函数中描述的那些注解的一种可替代方式。假设CommonAnnotationBeanPostProcessor
在Spring的ApplicationContext
中注册,执行这些注解的方法在生命周期的同一点被调用,作为对应的Spring生命周期接口方法或显式声明的回调方法。在下面的例子中,缓存会预先放置接近初始化之前,并在销毁之前清除。
1 | public class CachingMovieLister { |
关于组合各种生命周期机制的影响的更多细节,请看“组合生命周期机制”小节。